Class: FastlyCTL::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/fastlyctl/cli.rb,
lib/fastlyctl/commands/acl.rb,
lib/fastlyctl/commands/tls.rb,
lib/fastlyctl/commands/copy.rb,
lib/fastlyctl/commands/diff.rb,
lib/fastlyctl/commands/open.rb,
lib/fastlyctl/commands/clone.rb,
lib/fastlyctl/commands/login.rb,
lib/fastlyctl/commands/token.rb,
lib/fastlyctl/commands/watch.rb,
lib/fastlyctl/commands/domain.rb,
lib/fastlyctl/commands/upload.rb,
lib/fastlyctl/commands/logging.rb,
lib/fastlyctl/commands/snippet.rb,
lib/fastlyctl/commands/activate.rb,
lib/fastlyctl/commands/download.rb,
lib/fastlyctl/commands/skeleton.rb,
lib/fastlyctl/commands/condition.rb,
lib/fastlyctl/commands/purge_all.rb,
lib/fastlyctl/commands/dictionary.rb,
lib/fastlyctl/commands/create_service.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(a, b, c) ⇒ CLI

Returns a new instance of CLI.



27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/fastlyctl/cli.rb', line 27

def initialize(a,b,c)
  unless File.exist?(FastlyCTL::TOKEN_FILE) || ENV['FASTLYCLI_TOKEN']
    if yes?("Unable to locate API token. Would you like to login first?")
      self.
    end
  end

  super

  if options.key?(:debug)
    Typhoeus::Config.verbose = true
  end
end

Class Method Details



12
13
14
15
16
# File 'lib/fastlyctl/commands/condition.rb', line 12

def self.print_condition_header
  puts
  puts "Name".ljust(40) + " | " + "Priority".ljust(8) + " | " + "Type".ljust(10) + " | " + "Statement".ljust(20) 
  puts "-------------------------------------------------------------------------------------------------------"        
end


18
19
20
21
22
23
24
# File 'lib/fastlyctl/commands/condition.rb', line 18

def self.print_conditions(conditions)
  self.print_condition_header

  conditions.each { |c| 
    puts "%s | %s | %s | %s " % [c["name"].ljust(40), c["priority"].ljust(8), c["type"].ljust(10), c["statement"].ljust(20)]
  }
end

Instance Method Details

#acl(action, name = false, ip = false) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/fastlyctl/commands/acl.rb', line 14

def acl(action, name=false, ip=false)
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Specify service id with --service or use from within service directory." unless id

  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  version ||= options[:version]

  encoded_name = FastlyCTL::Utils.percent_encode(name) if name

  case action
  when "create"
    abort "Must specify name for ACL" unless name
    FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/version/#{version}/acl", params: { name: name })

    say("ACL #{name} created.")
  when "delete"
    abort "Must specify name for ACL" unless name
    FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/version/#{version}/acl/#{encoded_name}")

    say("ACL #{name} deleted.")
  when "list"
    resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl")

    say("No ACLs on service in this version.") unless resp.length > 0

    resp.each do |d|
      puts "#{d["id"]} - #{d["name"]}"
    end
  when "add"
    abort "Must specify name for ACL" unless name
    abort "Must specify IP" unless ip

    subnet = false
    if ip.include?("/")
      ip = ip.sub(/\/(\d{1,2})/,"")
      subnet = $1
    end

    acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}")

    params = {
      ip: ip,
      negated: options.key?(:negate) ? "1" : "0"
    }
    params[:subnet] = subnet if subnet

    FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/acl/#{acl["id"]}/entry", params: params)   

    say("#{ip} added to ACL #{name}.")    
  when "remove"
    abort "Must specify name for ACL" unless name
    abort "Must specify IP for ACL entry" unless ip
    acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}")
    entries = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/acl/#{acl["id"]}/entries")

    entry = false
    entries.each do |e|
      if e["ip"] == ip
        entry = e
        break
      end
    end

    abort "IP #{ip} not found in ACL" unless entry

    FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/acl/#{acl["id"]}/entry/#{entry["id"]}")

    say("IP #{ip} removed from ACL #{name}.")
  when "list_ips"
    abort "Must specify name for ACL" unless name
    acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}")
    entries = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/acl/#{acl["id"]}/entries")

    say("No items in ACL.") unless entries.length > 0
    entries.each do |i|
      puts "#{i["ip"]}#{i["subnet"].nil? ? "" : "/"+i["subnet"].to_s} - Negated: #{i["negated"] == "0" ? "false" : "true"}"
    end
  when "sync"
    abort "Must specify name for ACL" unless name
    abort "Must supply comma separated list of IPs as the \"ip\" parameter" unless ip

    ips = ip.split(',').to_set.to_a
    entry_ids = Hash.new
    current_ips = []

    acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}")
    entries = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/acl/#{acl["id"]}/entries")
    entries.each do |entry|
      ip_with_subnet = entry["ip"]
      ip_with_subnet += "/" + entry["subnet"].to_s if (entry.key?("subnet") && !entry["subnet"].nil?)

      entry_ids[ip_with_subnet] = entry["id"]
      current_ips.push(ip_with_subnet)
    end

    to_add = ips - current_ips
    to_remove = current_ips - ips

    bulk = []

    to_add.each do |add|
      subnet = false
      if add.include?("/")
        add = add.sub(/\/(\d{1,2})/,"")
        subnet = $1
      end

      params = {
        "op" => "create",
        "ip" => add
      }
      params["subnet"] = subnet if subnet

      bulk.push(params)
    end

    to_remove.each do |remove|
      entry_id = entry_ids[remove]
      remove = remove.sub(/\/(\d{1,2})/,"") if remove.include?("/")

      bulk.push({
        "op" => "delete",
        "id" => entry_id
      })
    end

    FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/acl/#{acl["id"]}/entries", {body: {entries: bulk}.to_json, headers: {"Content-Type" => "application/json"}})

    say("Sync operation completed successfully with #{bulk.length} operations.")

  when "bulk_add"
    abort "Must specify name for ACL" unless name
    abort "Must specify JSON blob of operations in ip field. Documentation on this can be found here: https://docs.fastly.com/api/config#acl_entry_c352ca5aee49b7898535cce488e3ba82" unless ip
    acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}")
    FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/acl/#{acl["id"]}/entries", {body: ip, headers: {"Content-Type" => "application/json"}})

    say("Bulk add operation completed successfully.")
  else
    abort "#{action} is not a valid command"
  end
end

#activateObject



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/fastlyctl/commands/activate.rb', line 7

def activate
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id

  writable_version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  writable_version ||= options[:version].to_i

  if options.key?(:comment)
    FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{writable_version}",{
      params: {comment: options[:comment]}
    })
  end

  FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{writable_version}/activate")

  say("Version #{writable_version} on #{id} activated.")
end

#clone(id, target_id) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/fastlyctl/commands/clone.rb', line 6

def clone(id,target_id)
  version = FastlyCTL::Fetcher.get_active_version(id) unless options[:version]
  version ||= options[:version]

  target_version = FastlyCTL::Fetcher.api_request(:post, "/service/#{target_id}/version")["number"]

  puts "Copying #{id} version #{version} to #{target_id} version #{target_version}..."

  FastlyCTL::CloneUtils::OBJECT_TYPES.each do |type,meta|
    next if (type.include?("logging/") && options.key?(:skip_logging))

    response = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/#{type}")
    response = [response] unless response.is_a?(Array)

    next unless response.length > 0

    puts "Copying #{response.length} " + (response.length == 1 ? FastlyCTL::CloneUtils.unpluralize(type) : FastlyCTL::CloneUtils.pluralize(type))

    response.each do |obj|
      FastlyCTL::CloneUtils.copy(obj,type,target_id,target_version)
    end
  end

  target_active_version = FastlyCTL::Fetcher.get_active_version(target_id)
  response = FastlyCTL::Fetcher.api_request(:get, "/service/#{target_id}/version/#{target_active_version}/domain")
  return unless response.length > 0

  puts "Restoring #{response.length} " + (response.length == 1 ? "domain" : "domains" + " from #{target_id} version #{target_active_version}...")

  response.each do |domain|
    FastlyCTL::CloneUtils.copy(domain,"domain",target_id,target_version)
  end
end

#condition(action, name = false) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/fastlyctl/commands/condition.rb', line 26

def condition(action,name=false)
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id

  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  version ||= options[:version].to_i

  encoded_name = FastlyCTL::Utils.percent_encode(name) if name

  case action 
      when "list"
          conditions =  FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/condition")
          CLI.print_conditions(conditions)

      when "create"
          abort "Must supply a condition name as second parameter" unless name
          abort "Must supply a statement to create a condition" unless options[:statement]

          params = {}
          params[:name] = name 
          params[:statement] = options[:statement]

          params[:priority] = options[:priority] if options.key?(:priority)
          params[:type] = options[:type] if options.key?(:type)
          params[:comment] = options[:comment] if options.key?(:comment) 

          FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/condition",{
              params: params
          })
          say("Condition #{name} created on #{id} version #{version}")

      when "update"
          abort "Must supply a condition name as second parameter" unless name

          if options.key?(:type)
              puts "WARNING: Can not change the TYPE of a condition, you must delete and re-create, type parameter is ignored in update method.\n"
          end

          params = {}
          params[:statement] = options[:statement] if options.key?(:statement)
          params[:priority] = options[:priority] if options.key?(:priority)
          params[:comment] = options[:comment] if options.key?(:comment) 

          FastlyCTL::Fetcher.api_request(:put,"/service/#{id}/version/#{version}/condition/#{encoded_name}",{
              params: params
          })
          say("Condition #{name} updated on #{id} version #{version}")    

      when "show"
          abort "Must supply a condition name as second parameter" unless name

          c =  FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/condition/#{encoded_name}")
          CLI.print_conditions([c])
          
      when "delete"
          abort "Must supply a condition name as second parameter" unless name

          c =  FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/condition/#{encoded_name}")
          say("Condition #{name} deleted on #{id} version #{version}")
         
  end
end

#copy(id, target_id, obj_type, obj_name = false) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/fastlyctl/commands/copy.rb', line 6

def copy(id,target_id,obj_type,obj_name=false)
  abort "Object name must be specified for all object types except settings" if (obj_name === false && obj_type != "settings")

  source_version = FastlyCTL::Fetcher.get_active_version(id) unless options[:version1]
  source_version ||= options[:version1]
  target_version = FastlyCTL::Fetcher.get_writable_version(target_id) unless options[:version2]
  target_version ||= options[:version2]

  unless FastlyCTL::CloneUtils::OBJECT_TYPES.include?(obj_type)
    abort "Object type #{obj_type} is invalid. Must be one of: #{FastlyCTL::CloneUtils::OBJECT_TYPES.keys.join(', ')}"
  end

  path = "/service/#{id}/version/#{source_version}/#{obj_type}"
  path += "/#{obj_name}" unless obj_type == "settings"
  obj = FastlyCTL::Fetcher.api_request(:get, path)

  encoded_name = FastlyCTL::Utils.percent_encode(obj_name)

  if (obj_type == "settings")
    puts "Copying settings from #{id} version #{source_version} to #{target_id} version #{target_version}..."
  else
    existing_obj = FastlyCTL::Fetcher.api_request(:get, "/service/#{target_id}/version/#{target_version}/#{obj_type}/#{encoded_name}",{
      expected_responses: [200,404]
    })

    if existing_obj.key?("name")
      abort unless yes?("A #{FastlyCTL::CloneUtils.unpluralize(obj_type)} named #{obj_name} already exists on #{target_id} version #{target_version}. Delete it and proceed?")

      FastlyCTL::Fetcher.api_request(:delete,"/service/#{target_id}/version/#{target_version}/#{obj_type}/#{encoded_name}")
    end

    puts "Copying #{FastlyCTL::CloneUtils.unpluralize(obj_type)} #{obj_name} from #{id} version #{source_version} to #{target_id} version #{target_version}..."
  end

  FastlyCTL::CloneUtils.copy(obj,obj_type,target_id,target_version)
end

#create_service(name) ⇒ Object



4
5
6
7
8
9
10
# File 'lib/fastlyctl/commands/create_service.rb', line 4

def create_service(name)
  service = FastlyCTL::Fetcher.api_request(:post, "/service", { params: { name: name }})

  if yes?("Service #{service["id"]} has been created. Would you like to open the configuration page?")
    FastlyCTL::Utils.open_service(service["id"])
  end
end

#dictionary(action, name = false, key = false, value = false) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/fastlyctl/commands/dictionary.rb', line 14

def dictionary(action, name=false, key=false, value=false)
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Specify service id with --service or use from within service directory." unless id

  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  version ||= options[:version]

  encoded_name = FastlyCTL::Utils.percent_encode(name) if name

  case action
  when "create"
    abort "Must specify name for dictionary" unless name

    params = { name: name }
    params[:write_only] = true if options.key?(:write_only)

    FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/version/#{version}/dictionary", params: params)

    say("Dictionary #{name} created.")
  when "delete"
    abort "Must specify name for dictionary" unless name
    FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")

    say("Dictionary #{name} deleted.")
  when "list"
    resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary")

    say("No dictionaries on service in this version.") unless resp.length > 0

    resp.each do |d|
      puts "#{d["id"]} - #{d["name"]}"
    end
  when "upsert"
    abort "Must specify name for dictionary" unless name
    abort "Must specify key and value for dictionary item" unless (key && value)
    dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
    FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/dictionary/#{dict["id"]}/item/#{FastlyCTL::Utils.percent_encode(key)}", params: { item_value: value })   

    say("Dictionary item #{key} set to #{value}.")   
  when "remove"
    abort "Must specify name for dictionary" unless name
    abort "Must specify key for dictionary item" unless key
    dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
    FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/dictionary/#{dict["id"]}/item/#{FastlyCTL::Utils.percent_encode(key)}")

    say("Item #{key} removed from dictionary #{name}.")
  when "list_items"
    abort "Must specify name for dictionary" unless name
    dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
    resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/dictionary/#{dict["id"]}/items")

    say("No items in dictionary.") unless resp.length > 0
    resp.each do |i|
      puts "#{i["item_key"]} : #{i["item_value"]}"
    end
  when "sync"
    abort "Must specify name for dictionary" unless name
    abort "Must supply comma separated list of keys and values as the \"key\" parameter. " unless key

    pairs = {}
    key.split(',').to_set.to_a.each do |kv|
      kv = kv.split("=") if kv.include?("=")
      kv = kv.split(":") if kv.include?(":")
      abort "Keys and values must be separated by an = or : symbol. Found \"#{kv}\"" unless kv.is_a?(Array)
      pairs[kv[0]] = kv[1]
    end
    item_ids = Hash.new
    bulk = []

    dictionary = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
    items = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/dictionary/#{dictionary["id"]}/items")
    items.each do |item|
      unless pairs.key?(item["item_key"])
        bulk.push({
          "op" => "delete",
          "item_key" => item["item_key"]
        })
        next
      end

      if (pairs[item["item_key"]] != item["item_value"])
        bulk.push({
          "op": "upsert",
          "item_key": item["item_key"],
          "item_value": item["item_value"]
        })
      end

      pairs.delete(item["item_key"])
    end

    pairs.each do |k,v|
      bulk.push({
        "op": "create",
        "item_key": k,
        "item_value": v
      })
    end

    FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/dictionary/#{dictionary["id"]}/items", {body: {items: bulk}.to_json, headers: {"Content-Type" => "application/json"}})

    say("Sync operation completed successfully with #{bulk.length} operations.")

  when "bulk_add"
    abort "Must specify name for dictionary" unless name
    abort "Must specify JSON blob of operations in key field. Documentation on this can be found here: https://docs.fastly.com/api/config#dictionary_item_dc826ce1255a7c42bc48eb204eed8f7f" unless key
    dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}")
    FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/dictionary/#{dict["id"]}/items", {body: key, headers: {"Content-Type" => "application/json"}})

    say("Bulk add operation completed successfully.")
  else
    abort "#{action} is not a valid command"
  end
end

#diffObject



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/fastlyctl/commands/diff.rb', line 9

def diff
  if options[:service1]
    service1 = options[:service1]
  else
    service1 = FastlyCTL::Utils.parse_directory
    abort "Could not parse service id from directory" unless service1
  end
  if options[:service2]
    service2 = options[:service2]
  else
    service2 = FastlyCTL::Utils.parse_directory

    # use service1 for both if unspecified
    service2 = service1 unless service2
  end

  # diffing different services - no references to local vcl here
  if service1 != service2
    version1 = options.key?(:version1) ? options[:version1] : FastlyCTL::Fetcher.get_active_version(service1)
    version2 = options.key?(:version2) ? options[:version2] : FastlyCTL::Fetcher.get_active_version(service2)
  end

  # diffing the same service
  if service1 == service2
    # if both are specified, diff them
    if options[:version1] && options[:version2]
      version1 = options[:version1]
      version2 = options[:version2]
    end
    # if version1 is not specified, diff local with version 2
    if !options[:version1] && options[:version2]
      version1 = false
      version2 = options[:version2]
    end
    # if version2 is not specified, diff local with version 1
    if options[:version1] && !options[:version2]
      version1 = options[:version1]
      version2 = false
    end
    if !options[:version1] && !options[:version2]
      # if neither are specified, diff local with active version
      version1 = FastlyCTL::Fetcher.get_active_version(service2)
      version2 = false
    end
  end

  say("Diffing#{options[:generated] ? " generated VCL for" : ""} #{service1} #{version1 ? "version "+version1.to_s : "local VCL"} with #{service2} #{version2 ? "version "+version2.to_s : "local VCL"}.")

  if version1
    v1_vcls = FastlyCTL::Fetcher.get_vcl(service1, version1,options[:generated])
  else
    abort "Cannot diff generated VCL with local VCL" if options[:generated]
    Dir.foreach(Dir.pwd) do |p|
      next unless File.file?(p)
      next unless p =~ /\.vcl$/

      v1_vcls ||= Array.new
      v1_vcls << {
        "name" => p.chomp(".vcl"),
        "content" => File.read(p)
      }
    end
  end

  if version2
    v2_vcls = FastlyCTL::Fetcher.get_vcl(service2, version2,options[:generated])
  else
    abort "Cannot diff generated VCL with local VCL" if options[:generated]
    Dir.foreach(Dir.pwd) do |p|
      next unless File.file?(p)
      next unless p =~ /\.vcl$/

      v2_vcls ||= Array.new
      v2_vcls << {
        "name" => p.chomp(".vcl"),
        "content" => File.read(p)
      }
    end
  end

  if options[:generated]
    say(FastlyCTL::Utils.diff_generated(v1_vcls,v2_vcls))
  else
    say(FastlyCTL::Utils.diff_versions(v1_vcls,v2_vcls))
  end

end

#domain(action, host = false) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/fastlyctl/commands/domain.rb', line 6

def domain(action,host=false)
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id

  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  version ||= options[:version].to_i

  case action
  when "create"
    FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/domain",{
      params: {
        name: host,
      }
    })
    say("#{host} created on #{id} version #{version}")
  when "delete"
    FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/domain/#{host}")
    say("#{host} deleted on #{id} version #{version}")
  when "list"
    domains = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/domain")
    say("Listing all domains for #{id} version #{version}")
    domains.each do |d|
      puts d["name"]
    end
  when "check"
    if host == "all"
      domains = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/domain/check_all")
    else
      domains = [FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/domain/#{host}/check")]
    end

    domains.each do |d|
      say("#{d[0]["name"]} -> #{d[1]}")
    end
  else
    abort "#{action} is not a valid command"
  end
end

#download(vcl_name = false) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/fastlyctl/commands/download.rb', line 7

def download(vcl_name=false)
  parsed_id = FastlyCTL::Utils.parse_directory

  if options[:service]
    abort "Already in a service directory, go up one level in order to specify"\
          "service id with --service." if parsed_id
    id = options[:service]
    parsed = false
  else
    abort "Could not parse service id from directory. Specify service id with "\
          "--service option or use from within service directory." unless parsed_id
    id = parsed_id
    parsed = true
  end

  service = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/details")

  version = FastlyCTL::Fetcher.get_active_version(id) unless options[:version]
  version ||= options[:version]

  if options[:generated]
    generated = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/generated_vcl")
    File.open("generated.vcl", 'w+') {|f| f.write(generated["content"]) }
    abort "Generated VCL for version #{version} written to generated.vcl."
  end

  vcl = FastlyCTL::Fetcher.get_vcl(id, version)
  snippet = FastlyCTL::Fetcher.get_snippets(id, version)

  sname = service["name"]
  if sname.include? "/"
    sname = sname.tr("/","_")
  end

  folder_name = parsed ? "./" : "#{sname} - #{service["id"]}/"
  Dir.mkdir(folder_name) unless (File.directory?(folder_name) || parsed)

  if vcl
    vcl.each do |v,k|
      next if (vcl_name && vcl_name != v["name"])

      filename = "#{folder_name}#{v["name"]}.vcl"

      if File.exist?(filename)
        unless yes?("Are you sure you want to overwrite #{filename}")
          say("Skipping #{filename}")
          next
        end
      end

      File.open(filename, 'w+') {|f| f.write(v["content"]) }

      say("VCL content for version #{version} written to #{filename}")
    end
  end

  if snippet
    snippet.each do |s,k|
      filename = "#{folder_name}#{s["name"]}.snippet"

      if File.exist?(filename)
        unless yes?("Are you sure you want to overwrite #{filename}")
          say("Skipping #{filename}")
          next
        end
      end

      File.open(filename, 'w+') {|f| f.write(s["content"]) }

      say("Snippet content for version #{version} written to #{filename}")
    end
  end

  unless vcl || snippet
    say("No VCLs or snippets on this service, however a folder has been created. Create VCLs in this folder and upload.")
  end
end

#loginObject



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/fastlyctl/commands/login.rb', line 4

def 
  if FastlyCTL::Token
    abort unless yes?("You already have an access token, are you sure you want to authenticate again?")
  end

  if yes?("Does your organization use SSO to login to Fastly? If so, type \"yes\" and create a 'global' or 'root' scoped token in your web browser. Copy the token to the file ~/.fastlyctl_token and save it.")
    Launchy.open(FastlyCTL::FASTLY_APP + "/account/personal/tokens/new")
    abort
  end

  say("Proceeding with username/password login...")

  username = ask("Username: ")
  password = ask("Password: ", :echo => false)
  say("")

  if username.include?("@fastly.com") && !username.include?("+")
    scope = "root"
  else
    scope = "global"
  end

  say("Creating #{scope} scope token...")

  o = {
    username: username,
    password: password,
    scope: scope,
    name: "fastlyctl_token"
  }

  resp = FastlyCTL::Fetcher.create_token(o)

  token = resp["access_token"]
  token_id = resp["id"]

  File.open(FastlyCTL::TOKEN_FILE , 'w+') {|f| f.write(token) }
  File.chmod(0600, FastlyCTL::TOKEN_FILE)

  resp = FastlyCTL::Fetcher.api_request(:get, "/tokens", { headers: {"Fastly-Key" => token}})
  abort unless resp.count > 0

  resp.each do |t|
    next unless (t["name"] == "fastlyctl_token" && t["id"] != token_id)

    if yes?("There was already a token created with the name fastlyctl_token. To avoid creating multiple tokens, should it be deleted?")
      FastlyCTL::Fetcher.api_request(:delete, "/tokens/#{t["id"]}", {headers: {"Fastly-Key" => token}, expected_responses: [204]})
      say("Token with id #{t["id"]} deleted.")
    end
  end
end

#open(domain = false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/fastlyctl/commands/open.rb', line 5

def open(domain=false)
  if (options[:service] && domain)
    say("Both domain and service id supplied, using service id.")
    domain = false
  end

  if options[:service]
    id = options[:service]
  elsif domain
    id = FastlyCTL::Fetcher.domain_to_service_id(domain)
  else
    id = FastlyCTL::Utils.parse_directory

    abort "Could not parse service id from directory. Use the --s <service> flag OR vcl open <domain>." unless id
  end

  FastlyCTL::Utils.open_service(id)
end

#purge_allObject



5
6
7
8
9
10
11
12
13
14
# File 'lib/fastlyctl/commands/purge_all.rb', line 5

def purge_all
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id

  FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/purge_all")

  say("Purge all on #{id} completed.")
end

#skeleton(name = "main") ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/fastlyctl/commands/skeleton.rb', line 5

def skeleton(name="main")
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]
  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id

  filename = "#{name}.vcl"
  version = FastlyCTL::Fetcher.get_active_version(id)
  boilerplate = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/boilerplate")

  if (File.exist?(filename))
    say("#{filename} exists, please delete it if you want this command to overwrite it.")
    abort
  end

  File.open(filename , 'w+') {|f| f.write(boilerplate) }

  say("Boilerplate written to #{filename}.")
end

#snippet(action, name = false) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/fastlyctl/commands/snippet.rb', line 12

def snippet(action,name=false)
  id = FastlyCTL::Utils.parse_directory unless options[:service]
  id ||= options[:service]

  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless id

  version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  version ||= options[:version].to_i

  encoded_name = FastlyCTL::Utils.percent_encode(name) if name

  filename = options.key?(:filename) ? options[:filename] : "#{name}.snippet"

  case action
  when "upload"
    abort "Must supply a snippet name as second parameter" unless name

    abort "No snippet file for #{name} found locally" unless File.exists?(filename)

    active_version = FastlyCTL::Fetcher.get_active_version(id)

    snippets = FastlyCTL::Fetcher.get_snippets(id, active_version)

    abort "No snippets found in active version" unless snippets.is_a?(Array) && snippets.length > 0

    snippet = false
    snippets.each do |s|
      if s["name"] == name
        abort "This command is for dynamic snippets only. Use vcl upload for versioned snippets" if s["dynamic"] == "0"

        snippet = s
      end
    end

    abort "No snippet named #{name} found on active version" unless snippet

    # get the snippet from the dynamic snippet api endpoint so you have the updated content
    snippet = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/snippet/#{snippet["id"]}")

    new_content = File.read(filename)
    priority = options.key(:priority) ? options[:priority] : snippet[:priority]

    unless options.key?(:yes)
      say(FastlyCTL::Utils.get_diff(snippet["content"],new_content))
      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!")
    end

    FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/snippet/#{snippet["snippet_id"]}", {:endpoint => :api, body: {
        content: new_content,
        priority: priority.to_s
      }
    })

    say("New snippet content for #{name} uploaded successfully")
  when "create"
    abort "Must supply a snippet name as second parameter" unless name

    content = "# Put snippet content here."

    FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/snippet",{
      params: {
        name: name,
        type: options[:type] ? options[:type] : "recv",
        content: content,
        dynamic: options.key?(:dynamic) ? 1 : 0,
        priority: options.key?(:priority) ? options[:priority].to_s : "100"
      }
    })
    say("#{name} created on #{id} version #{version}")

    unless File.exists?(filename)
      File.open(filename, 'w+') {|f| content }
      say("Blank snippet file created locally.")
      return
    end

    if options.key?(:yes) || yes?("Local file #{filename} found. Would you like to upload its content?")
      FastlyCTL::Fetcher.upload_snippet(id,version,File.read(filename),name)
      say("Local snippet file content successfully uploaded.")
    end
  when "delete"
    abort "Must supply a snippet name as second parameter" unless name

    FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/snippet/#{encoded_name}")
    say("#{name} deleted on #{id} version #{version}")

    return unless File.exists?(filename)

    if options.key?(:yes) || yes?("Would you like to delete the local file #{name}.snippet associated with this snippet?")
      File.delete(filename)
      say("Local snippet file #{filename} deleted.")
    end
  when "list"
    snippets = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/snippet")
    say("Listing all snippets for #{id} version #{version}")
    snippets.each do |d|
      say("#{d["name"]}: Subroutine: #{d["type"]}, Dynamic: #{d["dynamic"]}")
    end
  else
    abort "#{action} is not a valid command"
  end
end

#token(action) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/fastlyctl/commands/token.rb', line 7

def token(action)
  case action
  when "list"
    if options[:customer]
      tokens = FastlyCTL::Fetcher.api_request(:get, "/customer/#{options[:customer]}/tokens")
    else
      tokens = FastlyCTL::Fetcher.api_request(:get, "/tokens")
    end
    abort "No tokens to display!" unless tokens.length > 0

    pp tokens

  when "create"
    scope = options[:scope]
    scope ||= "global"

    say("You must authenticate again to create tokens.")

    username = ask("Username: ")
    password = ask("Password: ", :echo => false)
    say("")

    name = ask("What would you like to name your token? (enter here):")
    o = {
      username: username,
      password: password,
      scope: scope,
      name: name || "fastlyctl_token"
    }.compare_by_identity

    options[:services].split(",").each do |v|
      o["services[]"] = v
    end if options[:services]
    o[:customer] = options[:customer] if options[:customer]

    resp = FastlyCTL::Fetcher.create_token(o)
    say("token: #{resp["access_token"]}")

  when "delete"
    id = ask("What is the ID of the token you'd like to delete?")

    FastlyCTL::Fetcher.api_request(:delete, "/tokens/#{id}", expected_responses: [204])
    say("Token with id #{id} deleted.")
  else
    abort "#{action} is not a valid command"
  end
end

#uploadObject



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/fastlyctl/commands/upload.rb', line 6

def upload
  id = FastlyCTL::Utils.parse_directory

  abort "Could not parse service id from directory. Use -s <service> to specify, vcl download, then try again." unless id

  vcls = {}
  snippets = {}

  Dir.foreach(Dir.pwd) do |p|
    next unless File.file?(p)
    if p =~ /\.vcl$/
      vcls[p.chomp(".vcl")] = {"content" => File.read(p), "name" => p.chomp(".vcl")}
      next
    end

    if (p =~ /\.snippet$/)
      snippets[p.chomp(".snippet")] = {"content" => File.read(p), "name" => p.chomp(".snippet")}
    end
  end

  writable_version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version]
  writable_version ||= options[:version].to_i
  active_version = FastlyCTL::Fetcher.get_active_version(id);

  old_vcls = FastlyCTL::Fetcher.get_vcl(id, active_version)
  old_snippets = FastlyCTL::Fetcher.get_snippets(id, active_version)
  old_snippets_writable = FastlyCTL::Fetcher.get_snippets(id, writable_version)

  main_found = false

  old_vcls ||= {}
  old_vcls.each do |v|
    next unless vcls.has_key? v["name"]
    diff = FastlyCTL::Utils.get_diff(v["content"], vcls[v["name"]]["content"])

    vcls[v["name"]]["matched"] = true
    vcls[v["name"]]["new"] = false
    main_found = vcls[v["name"]]["main"] = v["main"] == true ? true : false
    vcls[v["name"]]["diff_length"] = diff.length

    next if diff.length < 2

    say(diff)
  end

  old_snippets ||= {}
  old_snippets.each do |s|
    next unless snippets.has_key? s["name"]
    diff = FastlyCTL::Utils.get_diff(s["content"], snippets[s["name"]]["content"])

    if s["dynamic"] == "1"
      snippets[s["name"]]["skip_because_dynamic"] = true
      next
    end

    snippets[s["name"]]["matched"] = true
    snippets[s["name"]]["diff_length"] = diff.length

    next if diff.length < 2

    say(diff)
  end
  old_snippets_writable ||= {}
  old_snippets_writable.each do |s|
    next unless snippets.has_key? s["name"]
    next if (old_snippets.select {|os| os["name"] == s["name"]}).length > 0

    if s["dynamic"] == "1"
      snippets[s["name"]]["skip_because_dynamic"] = true
      next
    end

    snippets[s["name"]]["matched"] = true
    snippets[s["name"]]["diff_length"] = 3

    say(FastlyCTL::Utils.get_diff("",snippets[s["name"]]["content"]))
  end

  vcls.delete_if do |k,v|
    if v["name"] == "generated"
      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?")
    end

    if (v["matched"] == true)
      #dont upload if the file isn't different from the old file
      if (v["diff_length"] > 1)
        false
      else
        true
      end
    elsif yes?("VCL #{v["name"]} does not currently exist on the service, would you like to create it?")
      v["new"] = true
      if !main_found
        v["main"] = true 
        main_found = true
      end
      say(FastlyCTL::Utils.get_diff("", v["content"]))
      false
    else 
      say("Not uploading #{v["name"]}")
      true
    end
  end

  snippets.delete_if do |k,s|
    if (s["matched"] == true)
      #dont upload if the file isn't different from the old file
      if (s["diff_length"] > 1)
        false
      else
        true
      end
    else
      if s.key?("skip_because_dynamic")
        true
      else
        say("Not uploading #{s["name"]} because it does not exist on the service. Use the \"snippet create\" command to create it.")
        true
      end
    end
  end

  abort unless yes?("Given the above diff, are you sure you want to upload your changes?")

  vcls.each do |k,v|
    FastlyCTL::Fetcher.upload_vcl(id, writable_version, v["content"], v["name"], v["main"], v["new"])

    say("#{v["name"]} uploaded to #{id}")
  end

  snippets.each do |k,s|
    FastlyCTL::Fetcher.upload_snippet(id, writable_version, s["content"], s["name"])

    say("#{s["name"]} uploaded to #{id}")
  end

  if options.key?(:comment)
    FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{writable_version}",{
      params: {comment: options[:comment]}
    })
  end

  validation = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{writable_version}/validate")

  abort "Compiler reported the following error with the generated VCL: #{validation["msg"]}" if validation["status"] == "error"

  say("VCL(s) have been uploaded to version #{writable_version} and validated.")
end

#versionObject



42
43
44
# File 'lib/fastlyctl/cli.rb', line 42

def version
  say("VCL gem version is #{FastlyCTL::VERSION}")
end

#watch(pop = false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/fastlyctl/commands/watch.rb', line 5

def watch(pop=false)
  service = options[:service]
  service ||= FastlyCTL::Utils.parse_directory

  abort "Could not parse service id from directory. Use --s <service> to specify, vcl download, then try again." unless service

  ts = false

  pop = pop.upcase if pop

  while true
    data = FastlyCTL::Fetcher.api_request(:get,"/rt/v1/channel/#{service}/ts/#{ts ? ts : 'h/limit/120'}", :endpoint => :rt)
    
    unless data["Data"].length > 0
      say("No data to display!")
      abort
    end

    if pop
      unless data["Data"][0]["datacenter"].key?(pop)
        abort "Could not locate #{pop} in data feed."
      end
      agg = data["Data"][0]["datacenter"][pop]
    else 
      agg = data["Data"][0]["aggregated"]
    end

    rps = agg["requests"]
    # gbps
    uncacheable = agg["pass"] + agg["synth"] + agg["errors"]
    bw = ((agg["resp_header_bytes"] + agg["resp_body_bytes"]).to_f * 8.0) / 1000000000.0
    shield = agg["shield"] || 0
    hit_rate = (1.0 - ((agg["miss"] - shield).to_f / ((agg["requests"] - uncacheable).to_f))) * 100.0
    passes = agg["pass"]
    miss_time = agg["miss"] > 0 ? ((agg["miss_time"] / agg["miss"]) * 1000).round(0) : 0
    synth = agg["synth"]
    errors = agg["errors"]

    $stdout.flush
    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"

    ts = data["Timestamp"]
  end
end