fastlyctl 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +324 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/build.rb +3 -0
- data/exe/fastlyctl +6 -0
- data/fastlyctl.gemspec +38 -0
- data/lib/fastlyctl/cli.rb +43 -0
- data/lib/fastlyctl/clone_utils.rb +149 -0
- data/lib/fastlyctl/commands/acl.rb +88 -0
- data/lib/fastlyctl/commands/activate.rb +27 -0
- data/lib/fastlyctl/commands/clone.rb +40 -0
- data/lib/fastlyctl/commands/copy.rb +41 -0
- data/lib/fastlyctl/commands/create_service.rb +18 -0
- data/lib/fastlyctl/commands/dictionary.rb +75 -0
- data/lib/fastlyctl/commands/diff.rb +97 -0
- data/lib/fastlyctl/commands/domain.rb +47 -0
- data/lib/fastlyctl/commands/download.rb +85 -0
- data/lib/fastlyctl/commands/login.rb +58 -0
- data/lib/fastlyctl/commands/open.rb +24 -0
- data/lib/fastlyctl/commands/purge_all.rb +17 -0
- data/lib/fastlyctl/commands/skeleton.rb +24 -0
- data/lib/fastlyctl/commands/snippet.rb +105 -0
- data/lib/fastlyctl/commands/token.rb +53 -0
- data/lib/fastlyctl/commands/upload.rb +155 -0
- data/lib/fastlyctl/commands/watch.rb +49 -0
- data/lib/fastlyctl/fetcher.rb +250 -0
- data/lib/fastlyctl/utils.rb +72 -0
- data/lib/fastlyctl/version.rb +3 -0
- data/lib/fastlyctl.rb +28 -0
- metadata +155 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
module FastlyCTL
|
2
|
+
class CLI < Thor
|
3
|
+
desc "snippet ACTION NAME", "Manipulate snippets on a service. Available actions are create, delete, and list. Use upload command to update snippets."
|
4
|
+
method_option :service, :aliases => ["--s"]
|
5
|
+
method_option :version, :aliases => ["--v"]
|
6
|
+
method_option :type, :aliases => ["--t"]
|
7
|
+
method_option :dynamic, :aliases => ["--d"]
|
8
|
+
def snippet(action,name=false)
|
9
|
+
id = FastlyCTL::Utils.parse_directory unless options[:service]
|
10
|
+
id ||= options[:service]
|
11
|
+
|
12
|
+
abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id
|
13
|
+
|
14
|
+
version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
|
15
|
+
version ||= options[:version].to_i
|
16
|
+
|
17
|
+
filename = "#{name}.snippet"
|
18
|
+
|
19
|
+
case action
|
20
|
+
when "upload"
|
21
|
+
abort "Must supply a snippet name as second parameter" unless name
|
22
|
+
|
23
|
+
abort "No snippet file for #{name} found locally" unless File.exists?(filename)
|
24
|
+
|
25
|
+
active_version = FastlyCTL::Fetcher.get_active_version(id)
|
26
|
+
|
27
|
+
snippets = FastlyCTL::Fetcher.get_snippets(id, active_version)
|
28
|
+
|
29
|
+
abort "No snippets found in active version" unless snippets.is_a?(Array) && snippets.length > 0
|
30
|
+
|
31
|
+
snippet = false
|
32
|
+
snippets.each do |s|
|
33
|
+
if s["name"] == name
|
34
|
+
abort "This command is for dynamic snippets only. Use vcl upload for versioned snippets" if s["dynamic"] == "0"
|
35
|
+
|
36
|
+
snippet = s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
abort "No snippet named #{name} found on active version" unless snippet
|
41
|
+
|
42
|
+
# get the snippet from the dynamic snippet api endpoint so you have the updated content
|
43
|
+
snippet = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/snippet/#{snippet["id"]}")
|
44
|
+
|
45
|
+
new_content = File.read(filename)
|
46
|
+
|
47
|
+
say(FastlyCTL::Utils.get_diff(snippet["content"],new_content))
|
48
|
+
|
49
|
+
abort unless yes?("Given the above diff between the old dyanmic snippet content and the new content, are you sure you want to upload your changes? REMEMBER, THIS SNIPPET IS VERSIONLESS AND YOUR CHANGES WILL BE LIVE IMMEDIATELY!")
|
50
|
+
|
51
|
+
FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/snippet/#{snippet["snippet_id"]}", {:endpoint => :api, body: {
|
52
|
+
content: new_content
|
53
|
+
}
|
54
|
+
})
|
55
|
+
|
56
|
+
say("New snippet content for #{name} uploaded successfully")
|
57
|
+
when "create"
|
58
|
+
abort "Must supply a snippet name as second parameter" unless name
|
59
|
+
|
60
|
+
content = "# Put snippet content here."
|
61
|
+
|
62
|
+
FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/snippet",{
|
63
|
+
params: {
|
64
|
+
name: name,
|
65
|
+
type: options[:type] ? options[:type] : "recv",
|
66
|
+
content: content,
|
67
|
+
dynamic: options.key?(:dynamic) ? 1 : 0
|
68
|
+
}
|
69
|
+
})
|
70
|
+
say("#{name} created on #{id} version #{version}")
|
71
|
+
|
72
|
+
unless File.exists?(filename)
|
73
|
+
File.open(filename, 'w+') {|f| content }
|
74
|
+
say("Blank snippet file created locally.")
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
if yes?("Local file #{filename} found. Would you like to upload its content?")
|
79
|
+
FastlyCTL::Fetcher.upload_snippet(id,version,File.read(filename),name)
|
80
|
+
say("Local snippet file content successfully uploaded.")
|
81
|
+
end
|
82
|
+
when "delete"
|
83
|
+
abort "Must supply a snippet name as second parameter" unless name
|
84
|
+
|
85
|
+
FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/snippet/#{name}")
|
86
|
+
say("#{name} deleted on #{id} version #{version}")
|
87
|
+
|
88
|
+
return unless File.exists?(filename)
|
89
|
+
|
90
|
+
if yes?("Would you like to delete the local file #{name}.snippet associated with this snippet?")
|
91
|
+
File.delete(filename)
|
92
|
+
say("Local snippet file #{filename} deleted.")
|
93
|
+
end
|
94
|
+
when "list"
|
95
|
+
snippets = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/snippet")
|
96
|
+
say("Listing all snippets for #{id} version #{version}")
|
97
|
+
snippets.each do |d|
|
98
|
+
say("#{d["name"]}: Subroutine: #{d["type"]}, Dynamic: #{d["dynamic"]}")
|
99
|
+
end
|
100
|
+
else
|
101
|
+
abort "#{action} is not a valid command"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module FastlyCTL
|
2
|
+
class CLI < Thor
|
3
|
+
desc "token ACTION", "Manipulate API tokens. Available actions are list, create, and delete. Scope defaults to admin:write. Options are --scope and --services. --services should be a comma separated list of services to restrict this token to."
|
4
|
+
method_option :customer, :aliases => ["--c"]
|
5
|
+
method_option :services, :aliases => ["--s"]
|
6
|
+
option :scope
|
7
|
+
def token(action)
|
8
|
+
case action
|
9
|
+
when "list"
|
10
|
+
if options[:customer]
|
11
|
+
tokens = FastlyCTL::Fetcher.api_request(:get, "/customer/#{options[:customer]}/tokens")
|
12
|
+
else
|
13
|
+
tokens = FastlyCTL::Fetcher.api_request(:get, "/tokens")
|
14
|
+
end
|
15
|
+
abort "No tokens to display!" unless tokens.length > 0
|
16
|
+
|
17
|
+
pp tokens
|
18
|
+
|
19
|
+
when "create"
|
20
|
+
scope = options[:scope]
|
21
|
+
scope ||= "global"
|
22
|
+
|
23
|
+
say("You must login again to create tokens.")
|
24
|
+
|
25
|
+
login_results = FastlyCTL::Fetcher.login
|
26
|
+
|
27
|
+
name = ask("What would you like to name your token?")
|
28
|
+
|
29
|
+
o = {
|
30
|
+
user: login_results[:user],
|
31
|
+
pass: login_results[:pass],
|
32
|
+
code: login_results[:code],
|
33
|
+
scope: scope,
|
34
|
+
name: name
|
35
|
+
}
|
36
|
+
|
37
|
+
o[:services] = options[:services].split(",") if options[:services]
|
38
|
+
|
39
|
+
o[:customer] = options[:customer] if options[:customer]
|
40
|
+
|
41
|
+
resp = FastlyCTL::Fetcher.create_token(o)
|
42
|
+
|
43
|
+
when "delete"
|
44
|
+
id = ask("What is the ID of the token you'd like to delete?")
|
45
|
+
|
46
|
+
FastlyCTL::Fetcher.api_request(:delete, "/tokens/#{id}", expected_responses: [204])
|
47
|
+
say("Token with id #{id} deleted.")
|
48
|
+
else
|
49
|
+
abort "#{action} is not a valid command"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module FastlyCTL
|
2
|
+
class CLI < Thor
|
3
|
+
desc "upload", "Uploads VCL in the current directory to the service."
|
4
|
+
method_option :version, :aliases => ["--v"]
|
5
|
+
method_option :comment, :aliases => ["--c"]
|
6
|
+
def upload
|
7
|
+
id = FastlyCTL::Utils.parse_directory
|
8
|
+
|
9
|
+
abort "Could not parse service id from directory. Use -s <service> to specify, vcl download, then try again." unless id
|
10
|
+
|
11
|
+
vcls = {}
|
12
|
+
snippets = {}
|
13
|
+
|
14
|
+
Dir.foreach(Dir.pwd) do |p|
|
15
|
+
next unless File.file?(p)
|
16
|
+
if p =~ /\.vcl$/
|
17
|
+
vcls[p.chomp(".vcl")] = {"content" => File.read(p), "name" => p.chomp(".vcl")}
|
18
|
+
next
|
19
|
+
end
|
20
|
+
|
21
|
+
if (p =~ /\.snippet$/)
|
22
|
+
snippets[p.chomp(".snippet")] = {"content" => File.read(p), "name" => p.chomp(".snippet")}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
writable_version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
|
27
|
+
writable_version ||= options[:version].to_i
|
28
|
+
active_version = FastlyCTL::Fetcher.get_active_version(id);
|
29
|
+
|
30
|
+
old_vcls = FastlyCTL::Fetcher.get_vcl(id, active_version)
|
31
|
+
old_snippets = FastlyCTL::Fetcher.get_snippets(id, active_version)
|
32
|
+
old_snippets_writable = FastlyCTL::Fetcher.get_snippets(id, writable_version)
|
33
|
+
|
34
|
+
main_found = false
|
35
|
+
|
36
|
+
old_vcls ||= {}
|
37
|
+
old_vcls.each do |v|
|
38
|
+
next unless vcls.has_key? v["name"]
|
39
|
+
diff = FastlyCTL::Utils.get_diff(v["content"], vcls[v["name"]]["content"])
|
40
|
+
|
41
|
+
vcls[v["name"]]["matched"] = true
|
42
|
+
vcls[v["name"]]["new"] = false
|
43
|
+
main_found = vcls[v["name"]]["main"] = v["main"] == true ? true : false
|
44
|
+
vcls[v["name"]]["diff_length"] = diff.length
|
45
|
+
|
46
|
+
next if diff.length < 2
|
47
|
+
|
48
|
+
say(diff)
|
49
|
+
end
|
50
|
+
|
51
|
+
old_snippets ||= {}
|
52
|
+
old_snippets.each do |s|
|
53
|
+
next unless snippets.has_key? s["name"]
|
54
|
+
diff = FastlyCTL::Utils.get_diff(s["content"], snippets[s["name"]]["content"])
|
55
|
+
|
56
|
+
if s["dynamic"] == "1"
|
57
|
+
snippets[s["name"]]["skip_because_dynamic"] = true
|
58
|
+
next
|
59
|
+
end
|
60
|
+
|
61
|
+
snippets[s["name"]]["matched"] = true
|
62
|
+
snippets[s["name"]]["diff_length"] = diff.length
|
63
|
+
|
64
|
+
next if diff.length < 2
|
65
|
+
|
66
|
+
say(diff)
|
67
|
+
end
|
68
|
+
old_snippets_writable ||= {}
|
69
|
+
old_snippets_writable.each do |s|
|
70
|
+
next unless snippets.has_key? s["name"]
|
71
|
+
next if (old_snippets.select {|os| os["name"] == s["name"]}).length > 0
|
72
|
+
|
73
|
+
if s["dynamic"] == "1"
|
74
|
+
snippets[s["name"]]["skip_because_dynamic"] = true
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
snippets[s["name"]]["matched"] = true
|
79
|
+
snippets[s["name"]]["diff_length"] = 3
|
80
|
+
|
81
|
+
say(FastlyCTL::Utils.get_diff("",snippets[s["name"]]["content"]))
|
82
|
+
end
|
83
|
+
|
84
|
+
vcls.delete_if do |k,v|
|
85
|
+
if v["name"] == "generated"
|
86
|
+
next unless yes?("The name of this file is 'generated.vcl'. Please do not upload generated VCL back to a service. Are you sure you want to upload this file?")
|
87
|
+
end
|
88
|
+
|
89
|
+
if (v["matched"] == true)
|
90
|
+
#dont upload if the file isn't different from the old file
|
91
|
+
if (v["diff_length"] > 1)
|
92
|
+
false
|
93
|
+
else
|
94
|
+
true
|
95
|
+
end
|
96
|
+
elsif yes?("VCL #{v["name"]} does not currently exist on the service, would you like to create it?")
|
97
|
+
v["new"] = true
|
98
|
+
if !main_found
|
99
|
+
v["main"] = true
|
100
|
+
main_found = true
|
101
|
+
end
|
102
|
+
say(FastlyCTL::Utils.get_diff("", v["content"]))
|
103
|
+
false
|
104
|
+
else
|
105
|
+
say("Not uploading #{v["name"]}")
|
106
|
+
true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
snippets.delete_if do |k,s|
|
111
|
+
if (s["matched"] == true)
|
112
|
+
#dont upload if the file isn't different from the old file
|
113
|
+
if (s["diff_length"] > 1)
|
114
|
+
false
|
115
|
+
else
|
116
|
+
true
|
117
|
+
end
|
118
|
+
else
|
119
|
+
if s.key?("skip_because_dynamic")
|
120
|
+
true
|
121
|
+
else
|
122
|
+
say("Not uploading #{s["name"]} because it does not exist on the service. Use the \"snippet create\" command to create it.")
|
123
|
+
true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
abort unless yes?("Given the above diff, are you sure you want to upload your changes?")
|
129
|
+
|
130
|
+
vcls.each do |k,v|
|
131
|
+
FastlyCTL::Fetcher.upload_vcl(id, writable_version, v["content"], v["name"], v["main"], v["new"])
|
132
|
+
|
133
|
+
say("#{v["name"]} uploaded to #{id}")
|
134
|
+
end
|
135
|
+
|
136
|
+
snippets.each do |k,s|
|
137
|
+
FastlyCTL::Fetcher.upload_snippet(id, writable_version, s["content"], s["name"])
|
138
|
+
|
139
|
+
say("#{s["name"]} uploaded to #{id}")
|
140
|
+
end
|
141
|
+
|
142
|
+
if options.key?(:comment)
|
143
|
+
FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{writable_version}",{
|
144
|
+
params: {comment: options[:comment]}
|
145
|
+
})
|
146
|
+
end
|
147
|
+
|
148
|
+
validation = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{writable_version}/validate")
|
149
|
+
|
150
|
+
abort "Compiler reported the following error with the generated VCL: #{validation["msg"]}" if validation["status"] == "error"
|
151
|
+
|
152
|
+
say("VCL(s) have been uploaded to version #{writable_version} and validated.")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module FastlyCTL
|
2
|
+
class CLI < Thor
|
3
|
+
desc "watch POP", "Watch live stats on a service. Optionally specify a POP by airport code."
|
4
|
+
method_option :service, :aliases => ["--s"]
|
5
|
+
def watch(pop=false)
|
6
|
+
service = options[:service]
|
7
|
+
service ||= FastlyCTL::Utils.parse_directory
|
8
|
+
|
9
|
+
abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless service
|
10
|
+
|
11
|
+
ts = false
|
12
|
+
|
13
|
+
pop = pop.upcase if pop
|
14
|
+
|
15
|
+
while true
|
16
|
+
data = FastlyCTL::Fetcher.api_request(:get,"/rt/v1/channel/#{service}/ts/#{ts ? ts : 'h/limit/120'}", :endpoint => :app)
|
17
|
+
|
18
|
+
unless data["Data"].length > 0
|
19
|
+
say("No data to display!")
|
20
|
+
abort
|
21
|
+
end
|
22
|
+
|
23
|
+
if pop
|
24
|
+
unless data["Data"][0]["datacenter"].key?(pop)
|
25
|
+
abort "Could not locate #{pop} in data feed."
|
26
|
+
end
|
27
|
+
agg = data["Data"][0]["datacenter"][pop]
|
28
|
+
else
|
29
|
+
agg = data["Data"][0]["aggregated"]
|
30
|
+
end
|
31
|
+
|
32
|
+
rps = agg["requests"]
|
33
|
+
# gbps
|
34
|
+
uncacheable = agg["pass"] + agg["synth"] + agg["errors"]
|
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
|
37
|
+
passes = agg["pass"]
|
38
|
+
miss_time = agg["miss"] > 0 ? ((agg["miss_time"] / agg["miss"]) * 1000).round(0) : 0
|
39
|
+
synth = agg["synth"]
|
40
|
+
errors = agg["errors"]
|
41
|
+
|
42
|
+
$stdout.flush
|
43
|
+
print " #{rps} req/s | #{bw.round(3)}gb/s | #{hit_rate.round(2)}% Hit Ratio | #{passes} passes/s | #{synth} synths/s | #{miss_time}ms miss time | #{errors} errors/s \r"
|
44
|
+
|
45
|
+
ts = data["Timestamp"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
module FastlyCTL
|
2
|
+
module Fetcher
|
3
|
+
def self.api_request(method, path, options={})
|
4
|
+
options[:endpoint] ||= :api
|
5
|
+
options[:params] ||= {}
|
6
|
+
options[:headers] ||= {}
|
7
|
+
options[:body] ||= nil
|
8
|
+
options[:force_session] ||= false
|
9
|
+
options[:expected_responses] ||= [200]
|
10
|
+
|
11
|
+
headers = {"Accept" => "application/json", "Connection" => "close", "User-Agent" => "FastlyCTL: https://github.com/fastly/fastlyctl"}
|
12
|
+
|
13
|
+
if options[:endpoint] == :app
|
14
|
+
headers["Referer"] = FastlyCTL::FASTLY_APP
|
15
|
+
headers["X-CSRF-Token"] = FastlyCTL::Cookies["fastly.csrf"] if FastlyCTL::Cookies["fastly.csrf"]
|
16
|
+
headers["Fastly-API-Request"] = "true"
|
17
|
+
end
|
18
|
+
|
19
|
+
if FastlyCTL::Token && !options[:force_session]
|
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
|
+
end
|
27
|
+
|
28
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded" if (method == :post || method == :put)
|
29
|
+
|
30
|
+
headers.merge!(options[:headers]) if options[:headers].count > 0
|
31
|
+
|
32
|
+
# dont allow header splitting on anything
|
33
|
+
headers.each do |k,v|
|
34
|
+
headers[k] = v.gsub(/\r|\n/,'')
|
35
|
+
end
|
36
|
+
|
37
|
+
url = "#{options[:endpoint] == :api ? FastlyCTL::FASTLY_API : FastlyCTL::FASTLY_APP}#{path}"
|
38
|
+
|
39
|
+
response = Typhoeus::Request.new(
|
40
|
+
url,
|
41
|
+
method: method,
|
42
|
+
params: options[:params],
|
43
|
+
headers: headers,
|
44
|
+
body: options[:body]
|
45
|
+
).run
|
46
|
+
|
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
|
56
|
+
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}."
|
67
|
+
end
|
68
|
+
|
69
|
+
error += " Method: #{method.to_s.upcase}, Path: #{path}\n"
|
70
|
+
error += "Message from API: #{response.response_body}"
|
71
|
+
|
72
|
+
abort error
|
73
|
+
end
|
74
|
+
|
75
|
+
return response.response_body if (response.headers["Content-Type"] != "application/json")
|
76
|
+
|
77
|
+
if response.response_body.length > 1
|
78
|
+
begin
|
79
|
+
return JSON.parse(response.response_body)
|
80
|
+
rescue JSON::ParserError
|
81
|
+
abort "Failed to parse JSON response from Fastly API"
|
82
|
+
end
|
83
|
+
else
|
84
|
+
return {}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.domain_to_service_id(domain)
|
89
|
+
response = Typhoeus::Request.new(FastlyCTL::FASTLY_APP, method:"FASTLYSERVICEMATCH", headers: { :host => domain}).run
|
90
|
+
|
91
|
+
abort "Failed to fetch Fastly service ID or service ID does not exist" if response.response_code != 204
|
92
|
+
|
93
|
+
abort "Fastly response did not contain service ID" unless response.headers["Fastly-Service-Id"]
|
94
|
+
|
95
|
+
return response.headers["Fastly-Service-Id"]
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.get_active_version(id)
|
99
|
+
service = self.api_request(:get, "/service/#{id}")
|
100
|
+
|
101
|
+
max = 1
|
102
|
+
|
103
|
+
service["versions"].each do |v|
|
104
|
+
if v["active"] == true
|
105
|
+
return v["number"]
|
106
|
+
end
|
107
|
+
|
108
|
+
max = v["number"] if v["number"] > max
|
109
|
+
end
|
110
|
+
|
111
|
+
return max
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.get_writable_version(id)
|
115
|
+
service = self.api_request(:get, "/service/#{id}")
|
116
|
+
|
117
|
+
active = false
|
118
|
+
version = false
|
119
|
+
max = 1
|
120
|
+
service["versions"].each do |v|
|
121
|
+
if v["active"] == true
|
122
|
+
active = v["number"].to_i
|
123
|
+
end
|
124
|
+
|
125
|
+
if active && v["number"].to_i > active && v["locked"] == false
|
126
|
+
version = v["number"]
|
127
|
+
end
|
128
|
+
|
129
|
+
max = version if version && version > max
|
130
|
+
end
|
131
|
+
|
132
|
+
return max unless active
|
133
|
+
|
134
|
+
version = self.api_request(:put, "/service/#{id}/version/#{active}/clone")["number"] unless version
|
135
|
+
|
136
|
+
return version
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.get_vcl(id, version, generated=false)
|
140
|
+
if generated
|
141
|
+
vcl = self.api_request(:get, "/service/#{id}/version/#{version}/generated_vcl")
|
142
|
+
else
|
143
|
+
vcl = self.api_request(:get, "/service/#{id}/version/#{version}/vcl?include_content=1")
|
144
|
+
end
|
145
|
+
|
146
|
+
if vcl.length == 0
|
147
|
+
return false
|
148
|
+
else
|
149
|
+
return vcl
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.get_snippets(id,version)
|
154
|
+
snippet = self.api_request(:get, "/service/#{id}/version/#{version}/snippet")
|
155
|
+
|
156
|
+
if snippet.length == 0
|
157
|
+
return false
|
158
|
+
else
|
159
|
+
return snippet
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.upload_snippet(service,version,content,name)
|
164
|
+
return FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/snippet/#{name}", {:endpoint => :api, body: {
|
165
|
+
content: content
|
166
|
+
}
|
167
|
+
})
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.upload_vcl(service,version,content,name,is_main=true,is_new=false)
|
171
|
+
params = { name: name, main: "#{is_main ? "1" : "0"}", content: content }
|
172
|
+
|
173
|
+
# try to create, if that fails, update
|
174
|
+
if is_new
|
175
|
+
response = FastlyCTL::Fetcher.api_request(:post, "/service/#{service}/version/#{version}/vcl", {:endpoint => :api, body: params, expected_responses:[200,409]})
|
176
|
+
if response["msg"] != "Duplicate record"
|
177
|
+
return
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
response = FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/vcl/#{name}", {:endpoint => :api, body: params, expected_responses: [200,404]})
|
182
|
+
|
183
|
+
# The VCL got deleted so recreate it.
|
184
|
+
if response["msg"] == "Record not found"
|
185
|
+
FastlyCTL::Fetcher.api_request(:post, "/service/#{service}/version/#{version}/vcl", {:endpoint => :api, body: params})
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
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
|
+
def self.create_token(options)
|
214
|
+
thor = Thor::Shell::Basic.new
|
215
|
+
|
216
|
+
headers = {}
|
217
|
+
headers["Fastly-OTP"] = options[:code] if options[:code]
|
218
|
+
|
219
|
+
FastlyCTL::Fetcher.api_request(:post, "/sudo", {
|
220
|
+
force_session: true,
|
221
|
+
endpoint: :api,
|
222
|
+
params: {
|
223
|
+
user: options[:user],
|
224
|
+
password: options[:pass]
|
225
|
+
},
|
226
|
+
headers: headers
|
227
|
+
})
|
228
|
+
|
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]
|
237
|
+
|
238
|
+
resp = FastlyCTL::Fetcher.api_request(:post, "/tokens", {
|
239
|
+
force_session: true,
|
240
|
+
endpoint: :api,
|
241
|
+
params: params,
|
242
|
+
headers: headers
|
243
|
+
})
|
244
|
+
|
245
|
+
thor.say("\n#{resp["id"]} created.")
|
246
|
+
|
247
|
+
return resp
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module FastlyCTL
|
2
|
+
module Utils
|
3
|
+
def self.open_service(id)
|
4
|
+
Launchy.open(FastlyCTL::FASTLY_APP + FastlyCTL::TANGO_PATH + id)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.parse_directory(path=false)
|
8
|
+
directory = Dir.pwd unless path
|
9
|
+
directory = path if path
|
10
|
+
|
11
|
+
id = directory.match(/.* \- ([^\-]*)$/i)
|
12
|
+
id = id == nil ? false : id.captures[0]
|
13
|
+
|
14
|
+
return id
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse_name(path=false)
|
18
|
+
directory = Dir.pwd unless path
|
19
|
+
directory = path if path
|
20
|
+
|
21
|
+
name = directory.match(/(.*) \- [^\-]*$/i)
|
22
|
+
name = name == nil ? false : name.captures[0]
|
23
|
+
|
24
|
+
return name
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.get_diff(old_vcl,new_vcl)
|
28
|
+
options = {
|
29
|
+
include_diff_info: true,
|
30
|
+
diff: ["-E", "-p"],
|
31
|
+
context: 3
|
32
|
+
}
|
33
|
+
return Diffy::Diff.new(old_vcl, new_vcl, options).to_s(:color)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.diff_generated(v1,v2)
|
37
|
+
diff = ""
|
38
|
+
|
39
|
+
diff << "\n" + self.get_diff(v1["content"], v2["content"])
|
40
|
+
|
41
|
+
return diff
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.diff_versions(v1,v2)
|
45
|
+
diff = ""
|
46
|
+
v1 ||= Array.new
|
47
|
+
v2 ||= Array.new
|
48
|
+
|
49
|
+
v1.each do |vcl1|
|
50
|
+
v2_content = false
|
51
|
+
|
52
|
+
v2.each do |vcl2|
|
53
|
+
v2_content = vcl2["content"] if (vcl1["name"] == vcl2["name"])
|
54
|
+
if (v2_content)
|
55
|
+
vcl2["matched"] = true
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
v2_content = "" unless v2_content
|
61
|
+
|
62
|
+
diff << "\n" + self.get_diff(vcl1["content"], v2_content)
|
63
|
+
end
|
64
|
+
|
65
|
+
v2.each do |vcl|
|
66
|
+
diff << "\n" + self.get_diff("", vcl["content"]) if !(vcl.has_key? "matched")
|
67
|
+
end
|
68
|
+
|
69
|
+
return diff
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|