├── .gitignore ├── Rakefile ├── lib ├── fastlyctl │ ├── version.rb │ ├── subcommand_patch.rb │ ├── commands │ │ ├── create_service.rb │ │ ├── purge_all.rb │ │ ├── open.rb │ │ ├── skeleton.rb │ │ ├── activate.rb │ │ ├── clone.rb │ │ ├── watch.rb │ │ ├── token.rb │ │ ├── domain.rb │ │ ├── login.rb │ │ ├── copy.rb │ │ ├── tls.rb │ │ ├── download.rb │ │ ├── logging.rb │ │ ├── diff.rb │ │ ├── condition.rb │ │ ├── tls │ │ │ └── managed.rb │ │ ├── snippet.rb │ │ ├── upload.rb │ │ ├── logging │ │ │ └── bigquery.rb │ │ ├── dictionary.rb │ │ └── acl.rb │ ├── cli.rb │ ├── utils.rb │ ├── clone_utils.rb │ └── fetcher.rb └── fastlyctl.rb ├── Gemfile ├── exe └── fastlyctl ├── bin ├── setup └── console ├── .github └── issue_template.md ├── Gemfile.lock ├── LICENSE.txt ├── fastlyctl.gemspec └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.DS_Store 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/fastlyctl/version.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | VERSION = "1.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in vcl.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /exe/fastlyctl: -------------------------------------------------------------------------------- 1 | #!ruby 2 | 3 | require "bundler/setup" 4 | require "fastlyctl" 5 | 6 | FastlyCTL::CLI.start(ARGV) 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/fastlyctl/subcommand_patch.rb: -------------------------------------------------------------------------------- 1 | class SubCommandBase < Thor 2 | def self.banner(command, namespace = nil, subcommand = false) 3 | basename + " " + self::SubcommandPrefix + " " + command.usage 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tracking issue 3 | about: DEPRECATED: FastlyCTL is no longer maintained or supported. Many features are already available in the official Fastly CLI (https://github.com/fastly/cli), which is actively maintained and developed. Please consider migrating if possible. 4 | --- 5 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "fastlyctl" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/create_service.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "create_service SERVICE_NAME", "Create a blank service." 4 | def create_service(name) 5 | service = FastlyCTL::Fetcher.api_request(:post, "/service", { params: { name: name }}) 6 | 7 | if yes?("Service #{service["id"]} has been created. Would you like to open the configuration page?") 8 | FastlyCTL::Utils.open_service(service["id"]) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/purge_all.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "purge_all", "Purge all content from a service." 4 | method_option :service, :aliases => ["--s"] 5 | def purge_all 6 | id = FastlyCTL::Utils.parse_directory unless options[:service] 7 | id ||= options[:service] 8 | 9 | abort "Could not parse service id from directory. Use --s to specify, vcl download, then try again." unless id 10 | 11 | FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/purge_all") 12 | 13 | say("Purge all on #{id} completed.") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | fastlyctl (1.0.14) 5 | diffy (~> 3.2.1) 6 | launchy (~> 2.4.3, >= 2.4.3) 7 | thor (~> 0.19.4) 8 | typhoeus (~> 1.3.1) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | addressable (2.6.0) 14 | public_suffix (>= 2.0.2, < 4.0) 15 | diffy (3.2.1) 16 | ethon (0.12.0) 17 | ffi (>= 1.3.0) 18 | ffi (1.11.1) 19 | launchy (2.4.3) 20 | addressable (~> 2.3) 21 | public_suffix (3.1.1) 22 | thor (0.19.4) 23 | typhoeus (1.3.1) 24 | ethon (>= 0.9.0) 25 | 26 | PLATFORMS 27 | ruby 28 | 29 | DEPENDENCIES 30 | fastlyctl! 31 | 32 | BUNDLED WITH 33 | 2.0.2 34 | -------------------------------------------------------------------------------- /lib/fastlyctl.rb: -------------------------------------------------------------------------------- 1 | require "typhoeus" 2 | require "thor" 3 | require "diffy" 4 | require "json" 5 | require "uri" 6 | require "launchy" 7 | require "erb" 8 | require "pp" 9 | require "openssl" 10 | 11 | require "fastlyctl/version" 12 | require "fastlyctl/fetcher" 13 | require "fastlyctl/clone_utils" 14 | require "fastlyctl/utils" 15 | require "fastlyctl/subcommand_patch" 16 | require "fastlyctl/cli" 17 | 18 | include ERB::Util 19 | 20 | module FastlyCTL 21 | TOKEN_FILE = ENV['HOME'] + "/.fastlyctl_token" 22 | FASTLY_API = "https://api.fastly.com" 23 | FASTLY_RT_API = "https://rt.fastly.com" 24 | TANGO_PATH = "/configure/services/" 25 | 26 | # Don't allow header splitting with the key 27 | Token = File.exist?(FastlyCTL::TOKEN_FILE) ? File.read(FastlyCTL::TOKEN_FILE) : (ENV['FASTLYCLI_TOKEN'] ? ENV['FASTLYCLI_TOKEN'] : false) 28 | end -------------------------------------------------------------------------------- /lib/fastlyctl/commands/open.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "open DOMAIN", "Find the service ID for a domain and open the Fastly app. You may also specify the service ID or assume the context of the directory you are in by omitting the domain." 4 | method_option :service, :aliases => ["--s"] 5 | def open(domain=false) 6 | if (options[:service] && domain) 7 | say("Both domain and service id supplied, using service id.") 8 | domain = false 9 | end 10 | 11 | if options[:service] 12 | id = options[:service] 13 | elsif domain 14 | id = FastlyCTL::Fetcher.domain_to_service_id(domain) 15 | else 16 | id = FastlyCTL::Utils.parse_directory 17 | 18 | abort "Could not parse service id from directory. Use the --s flag OR vcl open ." unless id 19 | end 20 | 21 | FastlyCTL::Utils.open_service(id) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/skeleton.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "skeleton NAME", "Create a skeleton VCL file with the current boilerplate." 4 | method_option :service, :aliases => ["--s"] 5 | def skeleton(name="main") 6 | id = FastlyCTL::Utils.parse_directory unless options[:service] 7 | id ||= options[:service] 8 | abort "Could not parse service id from directory. Use --s to specify, vcl download, then try again." unless id 9 | 10 | filename = "#{name}.vcl" 11 | version = FastlyCTL::Fetcher.get_active_version(id) 12 | boilerplate = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/boilerplate") 13 | 14 | if (File.exist?(filename)) 15 | say("#{filename} exists, please delete it if you want this command to overwrite it.") 16 | abort 17 | end 18 | 19 | File.open(filename , 'w+') {|f| f.write(boilerplate) } 20 | 21 | say("Boilerplate written to #{filename}.") 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stephen Basile 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/activate.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "activate", "Activates the latest writable service version, or the version number provided in the --version flag." 4 | method_option :service, :aliases => ["--s"] 5 | method_option :version, :aliases => ["--v"] 6 | method_option :comment, :aliases => ["--c"] 7 | def activate 8 | id = FastlyCTL::Utils.parse_directory unless options[:service] 9 | id ||= options[:service] 10 | 11 | abort "Could not parse service id from directory. Use --s to specify, vcl download, then try again." unless id 12 | 13 | writable_version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 14 | writable_version ||= options[:version].to_i 15 | 16 | if options.key?(:comment) 17 | FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{writable_version}",{ 18 | params: {comment: options[:comment]} 19 | }) 20 | end 21 | 22 | FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{writable_version}/activate") 23 | 24 | say("Version #{writable_version} on #{id} activated.") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/fastlyctl/cli.rb: -------------------------------------------------------------------------------- 1 | require "fastlyctl/commands/purge_all" 2 | require "fastlyctl/commands/open" 3 | require "fastlyctl/commands/download" 4 | require "fastlyctl/commands/diff" 5 | require "fastlyctl/commands/upload" 6 | require "fastlyctl/commands/activate" 7 | require "fastlyctl/commands/skeleton" 8 | require "fastlyctl/commands/clone" 9 | require "fastlyctl/commands/create_service" 10 | require "fastlyctl/commands/dictionary" 11 | require "fastlyctl/commands/login" 12 | require "fastlyctl/commands/watch" 13 | require "fastlyctl/commands/token" 14 | require "fastlyctl/commands/domain" 15 | require "fastlyctl/commands/snippet" 16 | require "fastlyctl/commands/acl" 17 | require "fastlyctl/commands/copy" 18 | require "fastlyctl/commands/logging" 19 | require "fastlyctl/commands/condition" 20 | require "fastlyctl/commands/tls" 21 | 22 | 23 | module FastlyCTL 24 | class CLI < Thor 25 | class_option :debug, :desc => 'Enabled debug mode output' 26 | 27 | def initialize(a,b,c) 28 | warn "DEPRECATED: FastlyCTL is no longer maintained or supported. Many features are already available in the official Fastly CLI (https://github.com/fastly/cli), which is actively maintained and developed. Please consider migrating if possible." 29 | 30 | unless File.exist?(FastlyCTL::TOKEN_FILE) || ENV['FASTLYCLI_TOKEN'] 31 | if yes?("Unable to locate API token. Would you like to login first?") 32 | self.login 33 | end 34 | end 35 | 36 | super 37 | 38 | if options.key?(:debug) 39 | Typhoeus::Config.verbose = true 40 | end 41 | end 42 | 43 | desc "version", "Displays version of the VCL gem." 44 | def version 45 | say("VCL gem version is #{FastlyCTL::VERSION}") 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /fastlyctl.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'fastlyctl/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "fastlyctl" 8 | spec.version = FastlyCTL::VERSION 9 | spec.authors = ["Stephen Basile"] 10 | spec.email = ["stephen@fastly.com"] 11 | 12 | spec.summary = %q{CLI tool for interacting with the Fastly API} 13 | spec.description = %q{DEPRECATED: FastlyCTL is no longer maintained or supported. Many features are already available in the official Fastly CLI (https://github.com/fastly/cli), which is actively maintained and developed. Please consider migrating if possible.} 14 | spec.homepage = "http://www.github.com/fastly/fastlyctl" 15 | spec.license = "MIT" 16 | 17 | spec.required_ruby_version = '>= 2.2.0' 18 | 19 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 20 | # delete this section to allow pushing this gem to any host. 21 | if spec.respond_to?(:metadata) 22 | spec.metadata['allowed_push_host'] = "https://rubygems.org/" 23 | else 24 | raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 25 | end 26 | 27 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.executables << "fastlyctl" 31 | spec.require_paths = ["lib"] 32 | 33 | spec.add_runtime_dependency "typhoeus", "~> 1.3.1" 34 | spec.add_runtime_dependency "thor", "~> 0.19.4" 35 | spec.add_runtime_dependency 'diffy', '~> 3.2.1' 36 | spec.add_runtime_dependency 'launchy', '~> 2.4.3', '>= 2.4.3' 37 | spec.add_runtime_dependency 'openssl', '~> 2.1.2', '>= 2.1.2' 38 | end 39 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/clone.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "clone SERVICE_ID TARGET_SERVICE_ID", "Clone a service version to another service." 4 | method_option :version, :aliases => ["--v"] 5 | method_option :skip_logging, :aliases => ["--sl"] 6 | def clone(id,target_id) 7 | version = FastlyCTL::Fetcher.get_active_version(id) unless options[:version] 8 | version ||= options[:version] 9 | 10 | target_version = FastlyCTL::Fetcher.api_request(:post, "/service/#{target_id}/version")["number"] 11 | 12 | puts "Copying #{id} version #{version} to #{target_id} version #{target_version}..." 13 | 14 | FastlyCTL::CloneUtils::OBJECT_TYPES.each do |type,meta| 15 | next if (type.include?("logging/") && options.key?(:skip_logging)) 16 | 17 | response = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/#{type}") 18 | response = [response] unless response.is_a?(Array) 19 | 20 | next unless response.length > 0 21 | 22 | puts "Copying #{response.length} " + (response.length == 1 ? FastlyCTL::CloneUtils.unpluralize(type) : FastlyCTL::CloneUtils.pluralize(type)) 23 | 24 | response.each do |obj| 25 | FastlyCTL::CloneUtils.copy(obj,type,target_id,target_version) 26 | end 27 | end 28 | 29 | target_active_version = FastlyCTL::Fetcher.get_active_version(target_id) 30 | response = FastlyCTL::Fetcher.api_request(:get, "/service/#{target_id}/version/#{target_active_version}/domain") 31 | return unless response.length > 0 32 | 33 | puts "Restoring #{response.length} " + (response.length == 1 ? "domain" : "domains" + " from #{target_id} version #{target_active_version}...") 34 | 35 | response.each do |domain| 36 | FastlyCTL::CloneUtils.copy(domain,"domain",target_id,target_version) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/watch.rb: -------------------------------------------------------------------------------- 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 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 => :rt) 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 | shield = agg["shield"] || 0 37 | hit_rate = (1.0 - ((agg["miss"] - shield).to_f / ((agg["requests"] - uncacheable).to_f))) * 100.0 38 | passes = agg["pass"] 39 | miss_time = agg["miss"] > 0 ? ((agg["miss_time"] / agg["miss"]) * 1000).round(0) : 0 40 | synth = agg["synth"] 41 | errors = agg["errors"] 42 | 43 | $stdout.flush 44 | 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" 45 | 46 | ts = data["Timestamp"] 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/token.rb: -------------------------------------------------------------------------------- 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 authenticate again to create tokens.") 24 | 25 | username = ask("Username: ") 26 | password = ask("Password: ", :echo => false) 27 | say("") 28 | 29 | name = ask("What would you like to name your token? (enter here):") 30 | o = { 31 | username: username, 32 | password: password, 33 | scope: scope, 34 | name: name || "fastlyctl_token" 35 | }.compare_by_identity 36 | 37 | options[:services].split(",").each do |v| 38 | o["services[]"] = v 39 | end if options[:services] 40 | o[:customer] = options[:customer] if options[:customer] 41 | 42 | resp = FastlyCTL::Fetcher.create_token(o) 43 | say("token: #{resp["access_token"]}") 44 | 45 | when "delete" 46 | id = ask("What is the ID of the token you'd like to delete?") 47 | 48 | FastlyCTL::Fetcher.api_request(:delete, "/tokens/#{id}", expected_responses: [204]) 49 | say("Token with id #{id} deleted.") 50 | else 51 | abort "#{action} is not a valid command" 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/domain.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "domain ACTION HOST", "Manipulate domains on a service. Available actions are create, delete, list and check. Create, delete and check take a host argument. Additionally, check can take the argument \"all\" to check all domains." 4 | method_option :service, :aliases => ["--s"] 5 | method_option :version, :aliases => ["--v"] 6 | def domain(action,host=false) 7 | id = FastlyCTL::Utils.parse_directory unless options[:service] 8 | id ||= options[:service] 9 | 10 | abort "Could not parse service id from directory. Use --s to specify, vcl download, then try again." unless id 11 | 12 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 13 | version ||= options[:version].to_i 14 | 15 | case action 16 | when "create" 17 | FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/domain",{ 18 | params: { 19 | name: host, 20 | } 21 | }) 22 | say("#{host} created on #{id} version #{version}") 23 | when "delete" 24 | FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/domain/#{host}") 25 | say("#{host} deleted on #{id} version #{version}") 26 | when "list" 27 | domains = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/domain") 28 | say("Listing all domains for #{id} version #{version}") 29 | domains.each do |d| 30 | puts d["name"] 31 | end 32 | when "check" 33 | if host == "all" 34 | domains = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/domain/check_all") 35 | else 36 | domains = [FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/domain/#{host}/check")] 37 | end 38 | 39 | domains.each do |d| 40 | say("#{d[0]["name"]} -> #{d[1]}") 41 | end 42 | else 43 | abort "#{action} is not a valid command" 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/login.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "login", "Logs into the app. Required before doing anything else." 4 | def login 5 | if FastlyCTL::Token 6 | abort unless yes?("You already have an access token, are you sure you want to authenticate again?") 7 | end 8 | 9 | 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.") 10 | Launchy.open(FastlyCTL::FASTLY_APP + "/account/personal/tokens/new") 11 | abort 12 | end 13 | 14 | say("Proceeding with username/password login...") 15 | 16 | username = ask("Username: ") 17 | password = ask("Password: ", :echo => false) 18 | say("") 19 | 20 | if username.include?("@fastly.com") && !username.include?("+") 21 | scope = "root" 22 | else 23 | scope = "global" 24 | end 25 | 26 | say("Creating #{scope} scope token...") 27 | 28 | o = { 29 | username: username, 30 | password: password, 31 | scope: scope, 32 | name: "fastlyctl_token" 33 | } 34 | 35 | resp = FastlyCTL::Fetcher.create_token(o) 36 | 37 | token = resp["access_token"] 38 | token_id = resp["id"] 39 | 40 | File.open(FastlyCTL::TOKEN_FILE , 'w+') {|f| f.write(token) } 41 | File.chmod(0600, FastlyCTL::TOKEN_FILE) 42 | 43 | resp = FastlyCTL::Fetcher.api_request(:get, "/tokens", { headers: {"Fastly-Key" => token}}) 44 | abort unless resp.count > 0 45 | 46 | resp.each do |t| 47 | next unless (t["name"] == "fastlyctl_token" && t["id"] != token_id) 48 | 49 | if yes?("There was already a token created with the name fastlyctl_token. To avoid creating multiple tokens, should it be deleted?") 50 | FastlyCTL::Fetcher.api_request(:delete, "/tokens/#{t["id"]}", {headers: {"Fastly-Key" => token}, expected_responses: [204]}) 51 | say("Token with id #{t["id"]} deleted.") 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/copy.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "copy SERVICE_ID TARGET_SERVICE_ID OBJECT_TYPE OBJECT_NAME", "Copy an object from one service to another\n Available Object Types: #{FastlyCTL::CloneUtils::OBJECT_TYPES.keys.join(', ')}" 4 | method_option :version1, :aliases => ["--v1"] 5 | method_option :version2, :aliases => ["--v2"] 6 | def copy(id,target_id,obj_type,obj_name=false) 7 | abort "Object name must be specified for all object types except settings" if (obj_name === false && obj_type != "settings") 8 | 9 | source_version = FastlyCTL::Fetcher.get_active_version(id) unless options[:version1] 10 | source_version ||= options[:version1] 11 | target_version = FastlyCTL::Fetcher.get_writable_version(target_id) unless options[:version2] 12 | target_version ||= options[:version2] 13 | 14 | unless FastlyCTL::CloneUtils::OBJECT_TYPES.include?(obj_type) 15 | abort "Object type #{obj_type} is invalid. Must be one of: #{FastlyCTL::CloneUtils::OBJECT_TYPES.keys.join(', ')}" 16 | end 17 | 18 | path = "/service/#{id}/version/#{source_version}/#{obj_type}" 19 | path += "/#{obj_name}" unless obj_type == "settings" 20 | obj = FastlyCTL::Fetcher.api_request(:get, path) 21 | 22 | encoded_name = FastlyCTL::Utils.percent_encode(obj_name) 23 | 24 | if (obj_type == "settings") 25 | puts "Copying settings from #{id} version #{source_version} to #{target_id} version #{target_version}..." 26 | else 27 | existing_obj = FastlyCTL::Fetcher.api_request(:get, "/service/#{target_id}/version/#{target_version}/#{obj_type}/#{encoded_name}",{ 28 | expected_responses: [200,404] 29 | }) 30 | 31 | if existing_obj.key?("name") 32 | abort unless yes?("A #{FastlyCTL::CloneUtils.unpluralize(obj_type)} named #{obj_name} already exists on #{target_id} version #{target_version}. Delete it and proceed?") 33 | 34 | FastlyCTL::Fetcher.api_request(:delete,"/service/#{target_id}/version/#{target_version}/#{obj_type}/#{encoded_name}") 35 | end 36 | 37 | puts "Copying #{FastlyCTL::CloneUtils.unpluralize(obj_type)} #{obj_name} from #{id} version #{source_version} to #{target_id} version #{target_version}..." 38 | end 39 | 40 | FastlyCTL::CloneUtils.copy(obj,obj_type,target_id,target_version) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/fastlyctl/utils.rb: -------------------------------------------------------------------------------- 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.open_app_path(path) 8 | Launchy.open(FastlyCTL::FASTLY_APP + path) 9 | end 10 | 11 | def self.parse_directory(path=false) 12 | directory = Dir.pwd unless path 13 | directory = path if path 14 | 15 | id = directory.match(/.* \- ([^\-]*)$/i) 16 | id = id == nil ? false : id.captures[0] 17 | 18 | return id 19 | end 20 | 21 | def self.parse_name(path=false) 22 | directory = Dir.pwd unless path 23 | directory = path if path 24 | 25 | name = directory.match(/(.*) \- [^\-]*$/i) 26 | name = name == nil ? false : name.captures[0] 27 | 28 | return name 29 | end 30 | 31 | def self.get_diff(old_vcl,new_vcl) 32 | options = { 33 | include_diff_info: true, 34 | diff: ["-E", "-p"], 35 | context: 3 36 | } 37 | return Diffy::Diff.new(old_vcl, new_vcl, options).to_s(:color) 38 | end 39 | 40 | def self.diff_generated(v1,v2) 41 | diff = "" 42 | 43 | diff << "\n" + self.get_diff(v1["content"], v2["content"]) 44 | 45 | return diff 46 | end 47 | 48 | def self.diff_versions(v1,v2) 49 | diff = "" 50 | v1 ||= Array.new 51 | v2 ||= Array.new 52 | 53 | v1.each do |vcl1| 54 | v2_content = false 55 | 56 | v2.each do |vcl2| 57 | v2_content = vcl2["content"] if (vcl1["name"] == vcl2["name"]) 58 | if (v2_content) 59 | vcl2["matched"] = true 60 | break 61 | end 62 | end 63 | 64 | v2_content = "" unless v2_content 65 | 66 | diff << "\n" + self.get_diff(vcl1["content"], v2_content) 67 | end 68 | 69 | v2.each do |vcl| 70 | diff << "\n" + self.get_diff("", vcl["content"]) if !(vcl.has_key? "matched") 71 | end 72 | 73 | return diff 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 91 | end 92 | end -------------------------------------------------------------------------------- /lib/fastlyctl/commands/tls.rb: -------------------------------------------------------------------------------- 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 71 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/download.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "download VCL_NAME", "Download VCLs. If no name is specified, downloads all the VCLs on the service." 4 | method_option :service, :aliases => ["--s"] 5 | method_option :version, :aliases => ["--v"] 6 | method_option :generated, :aliases => ["--g"] 7 | def download(vcl_name=false) 8 | parsed_id = FastlyCTL::Utils.parse_directory 9 | 10 | if options[:service] 11 | abort "Already in a service directory, go up one level in order to specify"\ 12 | "service id with --service." if parsed_id 13 | id = options[:service] 14 | parsed = false 15 | else 16 | abort "Could not parse service id from directory. Specify service id with "\ 17 | "--service option or use from within service directory." unless parsed_id 18 | id = parsed_id 19 | parsed = true 20 | end 21 | 22 | service = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/details") 23 | 24 | version = FastlyCTL::Fetcher.get_active_version(id) unless options[:version] 25 | version ||= options[:version] 26 | 27 | if options[:generated] 28 | generated = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/generated_vcl") 29 | File.open("generated.vcl", 'w+') {|f| f.write(generated["content"]) } 30 | abort "Generated VCL for version #{version} written to generated.vcl." 31 | end 32 | 33 | vcl = FastlyCTL::Fetcher.get_vcl(id, version) 34 | snippet = FastlyCTL::Fetcher.get_snippets(id, version) 35 | 36 | sname = service["name"] 37 | if sname.include? "/" 38 | sname = sname.tr("/","_") 39 | end 40 | 41 | folder_name = parsed ? "./" : "#{sname} - #{service["id"]}/" 42 | Dir.mkdir(folder_name) unless (File.directory?(folder_name) || parsed) 43 | 44 | if vcl 45 | vcl.each do |v,k| 46 | next if (vcl_name && vcl_name != v["name"]) 47 | 48 | filename = "#{folder_name}#{v["name"]}.vcl" 49 | 50 | if File.exist?(filename) 51 | unless yes?("Are you sure you want to overwrite #{filename}") 52 | say("Skipping #{filename}") 53 | next 54 | end 55 | end 56 | 57 | File.open(filename, 'w+') {|f| f.write(v["content"]) } 58 | 59 | say("VCL content for version #{version} written to #{filename}") 60 | end 61 | end 62 | 63 | if snippet 64 | snippet.each do |s,k| 65 | filename = "#{folder_name}#{s["name"]}.snippet" 66 | 67 | if File.exist?(filename) 68 | unless yes?("Are you sure you want to overwrite #{filename}") 69 | say("Skipping #{filename}") 70 | next 71 | end 72 | end 73 | 74 | File.open(filename, 'w+') {|f| f.write(s["content"]) } 75 | 76 | say("Snippet content for version #{version} written to #{filename}") 77 | end 78 | end 79 | 80 | unless vcl || snippet 81 | say("No VCLs or snippets on this service, however a folder has been created. Create VCLs in this folder and upload.") 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/logging.rb: -------------------------------------------------------------------------------- 1 | require "fastlyctl/commands/logging/bigquery" 2 | 3 | module FastlyCTL 4 | class LoggingSubCmd < Thor 5 | namespace :logging 6 | 7 | # bit of a monkey patch to fix --help output 8 | def self.banner(command, namespace = nil, subcommand = false) 9 | "#{basename} logging #{command.usage}" 10 | end 11 | 12 | 13 | desc "bigquery ", "Setup BigQuery As a logging provider, available actions are create, update, delete, list and show" 14 | method_option :service, :aliases => ["--s","--service"], :banner => "Service ID to use", :required => true 15 | method_option :version, :aliases => ["--v", "--version"], :banner => "Version of the service to use" 16 | method_option :name, :aliases => ["--n", "--name"], :banner => "Current name of the logging configuration" 17 | method_option :new_name, :aliases => ["--nn","--new-name"], :banner => "Used for the update method to rename a configuration" 18 | method_option :format_file, :aliases => ["--ff","--format-file"], :banner => "File containing the JSON Representation of the logline, must match BigQuery schema" 19 | method_option :format_version, :aliases => ["--fv","--format-version"], :banner => "Version of customer format, either 1 or 2, defaults to 2" 20 | method_option :user, :aliases => ["--u","--user"], :banner => "Google Cloud Service Account Email" 21 | method_option :secret_key_file, :aliases => ["--scf", "--secret-key-file"],:banner => "File that contains the Google Cloud Account secret key" 22 | method_option :project_id, :aliases => ["--p","--project-id"], :banner => "Google Cloud Project ID" 23 | method_option :dataset, :aliases => ["--d","--dataset"], :banner => "BigQuery Dataset" 24 | method_option :table, :aliases => ["--t","--table"], :banner => "BigQuery Table" 25 | method_option :template_suffix, :aliases => ["--ts","--template-suffix"], :banner => "Optional table name suffix" 26 | method_option :placement, :aliases => ["--pl","--placement"], :banner => "Placement of the logging call, can be none or waf_debug. Not required and no default" 27 | method_option :response_condition, :aliases => ["--r","--response-condition"], :banner => "When to execute, if empty it is always" 28 | 29 | def bigquery(action) 30 | case action 31 | when "create" 32 | BigQuery.create(options) 33 | when "list" 34 | BigQuery.list(options) 35 | when "update" 36 | BigQuery.update(options) 37 | when "show" 38 | BigQuery.show(options) 39 | when "delete" 40 | BigQuery.delete(options) 41 | else 42 | abort "Sorry, invalid action #{action} supplied, only create, update, delete and show are valid." 43 | end 44 | 45 | end 46 | 47 | # Placeholder for future S3 work 48 | # desc "s3 ", "Setup S3 as a logging provider" 49 | # method_option :format, :required => true 50 | # method_option :keyfile, :required => true 51 | # method_option :email , :required => true 52 | 53 | # def s3(action) 54 | # puts "S3: #{action}" 55 | # end 56 | 57 | desc "status", "Check the last output of the logging status" 58 | method_option :service, :aliases => ["--s","--service"], :banner => "Service ID to use", :required => true 59 | 60 | def status 61 | id = options[:service] 62 | resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/logging_status") 63 | say(JSON.pretty_generate(resp)) 64 | end 65 | 66 | end 67 | 68 | class CLI < Thor 69 | desc "logging SUBCOMMAND ...ARGS", "Interface with Fastly Logging" 70 | subcommand "logging", LoggingSubCmd 71 | end 72 | 73 | end -------------------------------------------------------------------------------- /lib/fastlyctl/commands/diff.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "diff", "Diff two service versions. By default, diffs the active version of the service assumed from the current directory with the local VCL in the current directory. Options allow you to specify different versions and different services." 4 | method_option :version1, :aliases => ["--v1"] 5 | method_option :version2, :aliases => ["--v2"] 6 | method_option :service1, :aliases => ["--s1"] 7 | method_option :service2, :aliases => ["--s2"] 8 | method_option :generated, :aliases => ["--g"] 9 | def diff 10 | if options[:service1] 11 | service1 = options[:service1] 12 | else 13 | service1 = FastlyCTL::Utils.parse_directory 14 | abort "Could not parse service id from directory" unless service1 15 | end 16 | if options[:service2] 17 | service2 = options[:service2] 18 | else 19 | service2 = FastlyCTL::Utils.parse_directory 20 | 21 | # use service1 for both if unspecified 22 | service2 = service1 unless service2 23 | end 24 | 25 | # diffing different services - no references to local vcl here 26 | if service1 != service2 27 | version1 = options.key?(:version1) ? options[:version1] : FastlyCTL::Fetcher.get_active_version(service1) 28 | version2 = options.key?(:version2) ? options[:version2] : FastlyCTL::Fetcher.get_active_version(service2) 29 | end 30 | 31 | # diffing the same service 32 | if service1 == service2 33 | # if both are specified, diff them 34 | if options[:version1] && options[:version2] 35 | version1 = options[:version1] 36 | version2 = options[:version2] 37 | end 38 | # if version1 is not specified, diff local with version 2 39 | if !options[:version1] && options[:version2] 40 | version1 = false 41 | version2 = options[:version2] 42 | end 43 | # if version2 is not specified, diff local with version 1 44 | if options[:version1] && !options[:version2] 45 | version1 = options[:version1] 46 | version2 = false 47 | end 48 | if !options[:version1] && !options[:version2] 49 | # if neither are specified, diff local with active version 50 | version1 = FastlyCTL::Fetcher.get_active_version(service2) 51 | version2 = false 52 | end 53 | end 54 | 55 | say("Diffing#{options[:generated] ? " generated VCL for" : ""} #{service1} #{version1 ? "version "+version1.to_s : "local VCL"} with #{service2} #{version2 ? "version "+version2.to_s : "local VCL"}.") 56 | 57 | if version1 58 | v1_vcls = FastlyCTL::Fetcher.get_vcl(service1, version1,options[:generated]) 59 | else 60 | abort "Cannot diff generated VCL with local VCL" if options[:generated] 61 | Dir.foreach(Dir.pwd) do |p| 62 | next unless File.file?(p) 63 | next unless p =~ /\.vcl$/ 64 | 65 | v1_vcls ||= Array.new 66 | v1_vcls << { 67 | "name" => p.chomp(".vcl"), 68 | "content" => File.read(p) 69 | } 70 | end 71 | end 72 | 73 | if version2 74 | v2_vcls = FastlyCTL::Fetcher.get_vcl(service2, version2,options[:generated]) 75 | else 76 | abort "Cannot diff generated VCL with local VCL" if options[:generated] 77 | Dir.foreach(Dir.pwd) do |p| 78 | next unless File.file?(p) 79 | next unless p =~ /\.vcl$/ 80 | 81 | v2_vcls ||= Array.new 82 | v2_vcls << { 83 | "name" => p.chomp(".vcl"), 84 | "content" => File.read(p) 85 | } 86 | end 87 | end 88 | 89 | if options[:generated] 90 | say(FastlyCTL::Utils.diff_generated(v1_vcls,v2_vcls)) 91 | else 92 | say(FastlyCTL::Utils.diff_versions(v1_vcls,v2_vcls)) 93 | end 94 | 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/condition.rb: -------------------------------------------------------------------------------- 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 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 -------------------------------------------------------------------------------- /lib/fastlyctl/commands/tls/managed.rb: -------------------------------------------------------------------------------- 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 ", "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 120 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/snippet.rb: -------------------------------------------------------------------------------- 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 | method_option :yes, :aliases => ["--y"] 9 | method_option :priority, :aliases => ["--p"] 10 | method_option :filename, :aliases => ["--f"] 11 | 12 | def snippet(action,name=false) 13 | id = FastlyCTL::Utils.parse_directory unless options[:service] 14 | id ||= options[:service] 15 | 16 | abort "Could not parse service id from directory. Use --s to specify, vcl download, then try again." unless id 17 | 18 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 19 | version ||= options[:version].to_i 20 | 21 | encoded_name = FastlyCTL::Utils.percent_encode(name) if name 22 | 23 | filename = options.key?(:filename) ? options[:filename] : "#{name}.snippet" 24 | 25 | case action 26 | when "upload" 27 | abort "Must supply a snippet name as second parameter" unless name 28 | 29 | abort "No snippet file for #{name} found locally" unless File.exists?(filename) 30 | 31 | active_version = FastlyCTL::Fetcher.get_active_version(id) 32 | 33 | snippets = FastlyCTL::Fetcher.get_snippets(id, active_version) 34 | 35 | abort "No snippets found in active version" unless snippets.is_a?(Array) && snippets.length > 0 36 | 37 | snippet = false 38 | snippets.each do |s| 39 | if s["name"] == name 40 | abort "This command is for dynamic snippets only. Use vcl upload for versioned snippets" if s["dynamic"] == "0" 41 | 42 | snippet = s 43 | end 44 | end 45 | 46 | abort "No snippet named #{name} found on active version" unless snippet 47 | 48 | # get the snippet from the dynamic snippet api endpoint so you have the updated content 49 | snippet = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/snippet/#{snippet["id"]}") 50 | 51 | new_content = File.read(filename) 52 | priority = options.key(:priority) ? options[:priority] : snippet[:priority] 53 | 54 | unless options.key?(:yes) 55 | say(FastlyCTL::Utils.get_diff(snippet["content"],new_content)) 56 | 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!") 57 | end 58 | 59 | FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/snippet/#{snippet["snippet_id"]}", {:endpoint => :api, body: { 60 | content: new_content, 61 | priority: priority.to_s 62 | } 63 | }) 64 | 65 | say("New snippet content for #{name} uploaded successfully") 66 | when "create" 67 | abort "Must supply a snippet name as second parameter" unless name 68 | 69 | content = "# Put snippet content here." 70 | 71 | FastlyCTL::Fetcher.api_request(:post,"/service/#{id}/version/#{version}/snippet",{ 72 | params: { 73 | name: name, 74 | type: options[:type] ? options[:type] : "recv", 75 | content: content, 76 | dynamic: options.key?(:dynamic) ? 1 : 0, 77 | priority: options.key?(:priority) ? options[:priority].to_s : "100" 78 | } 79 | }) 80 | say("#{name} created on #{id} version #{version}") 81 | 82 | unless File.exists?(filename) 83 | File.open(filename, 'w+') {|f| content } 84 | say("Blank snippet file created locally.") 85 | return 86 | end 87 | 88 | if options.key?(:yes) || yes?("Local file #{filename} found. Would you like to upload its content?") 89 | FastlyCTL::Fetcher.upload_snippet(id,version,File.read(filename),name) 90 | say("Local snippet file content successfully uploaded.") 91 | end 92 | when "delete" 93 | abort "Must supply a snippet name as second parameter" unless name 94 | 95 | FastlyCTL::Fetcher.api_request(:delete,"/service/#{id}/version/#{version}/snippet/#{encoded_name}") 96 | say("#{name} deleted on #{id} version #{version}") 97 | 98 | return unless File.exists?(filename) 99 | 100 | if options.key?(:yes) || yes?("Would you like to delete the local file #{name}.snippet associated with this snippet?") 101 | File.delete(filename) 102 | say("Local snippet file #{filename} deleted.") 103 | end 104 | when "list" 105 | snippets = FastlyCTL::Fetcher.api_request(:get,"/service/#{id}/version/#{version}/snippet") 106 | say("Listing all snippets for #{id} version #{version}") 107 | snippets.each do |d| 108 | say("#{d["name"]}: Subroutine: #{d["type"]}, Dynamic: #{d["dynamic"]}") 109 | end 110 | else 111 | abort "#{action} is not a valid command" 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/upload.rb: -------------------------------------------------------------------------------- 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 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 156 | -------------------------------------------------------------------------------- /lib/fastlyctl/clone_utils.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | module CloneUtils 3 | OBJECT_TYPES = { 4 | "condition" => {}, 5 | "acl" => {child: "entry", include_version: false}, 6 | "healthcheck" => {}, 7 | "cache_settings" => {}, 8 | "backend" => {}, 9 | "director" => {}, 10 | "dictionary" => {child: "item", include_version: false}, 11 | "gzip" => {}, 12 | "header" => {}, 13 | "request_settings" => {}, 14 | "response_object" => {}, 15 | "settings" => {method: :put}, 16 | "vcl" => {}, 17 | "snippet" => {}, 18 | "logging/s3" => {}, 19 | "logging/azureblob" => {}, 20 | "logging/cloudfiles" => {}, 21 | "logging/digitalocean" => {}, 22 | "logging/ftp" => {}, 23 | "logging/bigquery" => {}, 24 | "logging/gcs" => {}, 25 | "logging/honeycomb" => {}, 26 | "logging/logshuttle" => {}, 27 | "logging/logentries" => {}, 28 | "logging/loggly" => {}, 29 | "logging/heroku" => {}, 30 | "logging/openstack" => {}, 31 | "logging/papertrail" => {}, 32 | "logging/scalyr" => {}, 33 | "logging/splunk" => {}, 34 | "logging/sumologic" => {}, 35 | "logging/syslog" => {} 36 | } 37 | 38 | def self.copy(obj,type,sid,version) 39 | meta = FastlyCTL::CloneUtils::OBJECT_TYPES[type] 40 | meta ||= {} 41 | 42 | abort "No service ID on object" unless obj.key?("service_id") 43 | source_sid = obj["service_id"] 44 | abort "No version on object" unless obj.key?("version") 45 | source_version = obj["version"] 46 | main = false 47 | 48 | backends = obj["backends"].dup if type == "director" 49 | main = true if type == "vcl" && obj["main"] === true 50 | 51 | if type == "snippet" && obj["dynamic"] == "1" 52 | obj.merge!(FastlyCTL::Fetcher.api_request(:get, "/service/#{source_sid}/snippet/#{obj["id"]}")) 53 | end 54 | 55 | obj_id = obj["id"] 56 | obj = FastlyCTL::CloneUtils.filter(type,obj) 57 | 58 | obj = FastlyCTL::Fetcher.api_request(meta.key?(:method) ? meta[:method] : :post, "/service/#{sid}/version/#{version}/#{type}", body: obj ) 59 | 60 | if main === true 61 | # the "main-ness" of the vcl does not get carried over during creation. must explicitly set main 62 | FastlyCTL::Fetcher.api_request(:put, "/service/#{sid}/version/#{version}/vcl/#{FastlyCTL::Utils.percent_encode(obj["name"])}/main") 63 | end 64 | 65 | if type == "director" 66 | backends.each do |b| 67 | FastlyCTL::Fetcher.api_request(:post, "/service/#{sid}/version/#{version}/director/#{FastlyCTL::Utils.percent_encode(obj["name"])}/backend/#{b}", body: obj ) 68 | end 69 | end 70 | 71 | if type == "dictionary" && obj["write_only"] === true 72 | puts "Unable to clone the contents of a write only dictionary. Creating empty dictionary instead..." 73 | return obj 74 | end 75 | 76 | return obj unless meta.key?(:child) 77 | new_obj_id = obj["id"] 78 | child = meta[:child] 79 | 80 | path = FastlyCTL::CloneUtils.construct_path(source_sid,source_version,meta[:include_version]) 81 | 82 | items = [] 83 | entries = [] 84 | # build some batch requests for dictionaries and ACLs to save on API rate limit 85 | FastlyCTL::Fetcher.api_request(:get, "#{path}/#{type}/#{obj_id}/#{FastlyCTL::CloneUtils.pluralize(child)}").each do |child_obj| 86 | case child 87 | when "item" 88 | items.push({ 89 | "op" => "create","item_key" => child_obj["item_key"],"item_value" => child_obj["item_value"] 90 | }) 91 | next 92 | when "entry" 93 | entries.push({ 94 | "op" => "create","ip" => child_obj["ip"],"subnet" => child_obj["subnet"], "negate" => child_obj["negate"] 95 | }) 96 | next 97 | end 98 | 99 | child_obj = FastlyCTL::CloneUtils.filter(type,child_obj) 100 | 101 | path = FastlyCTL::CloneUtils.construct_path(sid,version,meta[:include_version]) 102 | 103 | FastlyCTL::Fetcher.api_request(:post, "#{path}/#{type}/#{new_obj_id}/#{child}", body: child_obj ) 104 | end 105 | 106 | FastlyCTL::Fetcher.api_request(:patch, "/service/#{sid}/dictionary/#{new_obj_id}/items", body: {"items" => items}.to_json, headers: {"Content-Type" => "application/json"} ) if items.length > 0 107 | FastlyCTL::Fetcher.api_request(:patch, "/service/#{sid}/acl/#{new_obj_id}/entries", body: {"entries" => entries}.to_json, headers: {"Content-Type" => "application/json"} ) if entries.length > 0 108 | 109 | return obj 110 | end 111 | 112 | def self.filter(type,obj) 113 | filter_keys = ["version","id","service_id","created_at","updated_at","deleted_at","locked"] 114 | 115 | obj.delete_if { |key, value| filter_keys.include?(key) } 116 | obj.delete_if { |key, value| value.nil? } 117 | 118 | case type 119 | when "backend" 120 | obj.delete("ipv4") 121 | obj.delete("hostname") 122 | obj.delete("ipv6") 123 | when "director" 124 | obj.delete("backends") 125 | when "snippet" 126 | obj.delete("snippet_id") 127 | when "settings" 128 | # this is to account for a bug in the API which disallows even setting this to zero 129 | obj.delete("general.default_pci") if obj["general.default_pci"] == 0 130 | end 131 | 132 | return obj 133 | end 134 | 135 | def self.construct_path(id,version,include_version) 136 | if include_version != false 137 | path = "/service/#{id}/version/#{version}" 138 | else 139 | path = "/service/#{id}" 140 | end 141 | 142 | return path 143 | end 144 | 145 | def self.unpluralize(type) 146 | type = type.dup 147 | type.sub!(/ies/,"y") 148 | type.sub!(/s$/,"") 149 | return type 150 | end 151 | 152 | def self.pluralize(type) 153 | type = type.dup 154 | type += "s" unless type[-1] == "s" 155 | type.sub!(/ys$/,"ies") 156 | return type 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/logging/bigquery.rb: -------------------------------------------------------------------------------- 1 | module BigQuery 2 | 3 | def BigQuery.parse_secret_key(file) 4 | key = File.read(file) 5 | if key[0..26] != "-----BEGIN PRIVATE KEY-----" 6 | abort "Error, private key file should begin with -----BEGIN PRIVATE KEY-----" 7 | end 8 | 9 | return key 10 | end 11 | 12 | def BigQuery.ensure_opts(required_opts,options) 13 | required_opts.each { |k| 14 | if !options.key?(k) 15 | abort "Error, option #{k.to_s} is required for this action" 16 | end 17 | } 18 | end 19 | 20 | def BigQuery.print_configs(config) 21 | max = {} 22 | max["name"] = 0 23 | max["dataset"] = 0 24 | max["table"] = 0 25 | max["project_id"] = 0 26 | fields = ["name","dataset","table","project_id"] 27 | 28 | config.each { |c| 29 | fields.each { |f| 30 | max[f] = c[f].length > max[f] ? c[f].length : max[f] 31 | } 32 | } 33 | 34 | puts 35 | puts "Name".ljust(max["name"]) + " | " + "Dataset".ljust(max["dataset"]) + " | " + "Table".ljust(max["table"]) + " | " + "ProjectId".ljust(max["project_id"]) 36 | puts "-" * (max["name"] + max["dataset"] + max["table"] + max["project_id"]) 37 | config.each { |c| 38 | puts "%s | %s | %s | %s" % [c["name"].ljust(max["name"]), c["dataset"].ljust(max["dataset"]), c["table"].ljust(max["table"]), c["project_id"].ljust(max["project_id"])] 39 | } 40 | puts 41 | end 42 | 43 | 44 | def self.create(options) 45 | puts "Creating bigquery log endpoint" 46 | required_opts = ["name", "format_file", "user", "secret_key_file", "project_id", "dataset", "table" ] 47 | 48 | ensure_opts(required_opts,options) 49 | 50 | parsed_key = parse_secret_key(options[:secret_key_file]) 51 | 52 | parsed_format = File.read(options[:format_file]) 53 | 54 | params = {} 55 | 56 | id = options[:service] 57 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 58 | version ||= options[:version] 59 | 60 | params[:name] = options[:name] 61 | params[:format] = parsed_format 62 | params[:format_version] = options[:format_version] unless options[:format_version].nil? 63 | params[:user] = options[:user] 64 | params[:secret_key] = parsed_key 65 | params[:project_id] = options[:project_id] 66 | params[:dataset] = options[:dataset] 67 | params[:table] = options[:table] 68 | params[:template_suffix] = options[:template_suffix] unless options[:template_suffix].nil? 69 | params[:placement] = options[:placement] unless options[:placement].nil? 70 | params[:response_condition] = options[:response_condition] unless options[:response_condition].nil? 71 | 72 | FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/version/#{version}/logging/bigquery", body: params) 73 | puts "BigQuery logging provider created in service id #{id} on version #{version}" 74 | end 75 | 76 | def self.update(options) 77 | required_opts = ["name"] 78 | ensure_opts(required_opts,options) 79 | 80 | puts "Updating bigquery log endpoint #{options[:name]}" 81 | 82 | parsed_key = parse_secret_key(options[:secret_key_file]) unless options[:secret_key_file].nil? 83 | parsed_format = File.read(options[:format_file]) unless options[:format_file].nil? 84 | 85 | params = {} 86 | 87 | id = options[:service] 88 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 89 | version ||= options[:version] 90 | 91 | params[:name] = options[:new_name] unless options[:new_name].nil? 92 | params[:format] = parsed_format unless options[:format_file].nil? 93 | params[:format_version] = options[:format_version] unless options[:format_version].nil? 94 | params[:user] = options[:user] unless options[:user].nil? 95 | params[:secret_key] = parsed_key unless options[:secret_key_file].nil? 96 | params[:project_id] = options[:project_id] unless options[:project_id].nil? 97 | params[:dataset] = options[:dataset] unless options[:dataset].nil? 98 | params[:table] = options[:table] unless options[:table].nil? 99 | params[:template_suffix] = options[:template_suffix] unless options[:template_suffix].nil? 100 | params[:placement] = options[:placement] unless options[:placement].nil? 101 | params[:response_condition] = options[:response_condition] unless options[:response_condition].nil? 102 | 103 | FastlyCTL::Fetcher.api_request(:put, "/service/#{id}/version/#{version}/logging/bigquery/#{options[:name]}", body: params) 104 | puts "BigQuery logging provider update in service id #{id} on version #{version}" 105 | end 106 | 107 | def self.list(options) 108 | id = options[:service] 109 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 110 | version ||= options[:version] 111 | 112 | puts "Listing all BigQuery configurations for service #{id} version #{version}" 113 | 114 | configs = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/logging/bigquery") 115 | print_configs(configs) 116 | end 117 | 118 | def self.show(options) 119 | required_opts = ["name"] 120 | ensure_opts(required_opts,options) 121 | id = options[:service] 122 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 123 | version ||= options[:version] 124 | 125 | resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/logging/bigquery/#{options[:name]}") 126 | puts JSON.pretty_generate(resp) 127 | end 128 | 129 | def self.delete(options) 130 | required_opts = ["version","name"] 131 | ensure_opts(required_opts,options) 132 | 133 | id = options[:service] 134 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 135 | version ||= options[:version] 136 | 137 | resp = FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/version/#{version}/logging/bigquery/#{options[:name]}") 138 | puts JSON.pretty_generate(resp) 139 | end 140 | 141 | end -------------------------------------------------------------------------------- /lib/fastlyctl/commands/dictionary.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "dictionary ACTION DICTIONARY_NAME KEY VALUE", "Manipulate edge dictionaries.\n Actions:\n create: Create a dictionary\n 4 | delete: Delete a dictionary\n 5 | list: Provide a list of dictionaries on this service\n 6 | upsert: Update a key in a dictionary if it exists. Add the key if it does not.\n 7 | remove: Remove a key from a dictionary\n 8 | list_items: List all keys in the dictionary\n 9 | sync: Synchronizes a dictionary with a comma separated list of key/value pairs. Will create, delete, or update keys as needed. Separate keys and values with = or :.\n 10 | bulk_add: Perform operations on the dictionary in bulk. A list of operations in JSON format should be specified in the key field. Documentation on this format can be found here: https://docs.fastly.com/api/config#dictionary_item_dc826ce1255a7c42bc48eb204eed8f7f" 11 | method_option :service, :aliases => ["--s"] 12 | method_option :version, :aliases => ["--v"] 13 | method_option :write_only, :aliases => ["--wo"] 14 | def dictionary(action, name=false, key=false, value=false) 15 | id = FastlyCTL::Utils.parse_directory unless options[:service] 16 | id ||= options[:service] 17 | 18 | abort "Could not parse service id from directory. Specify service id with --service or use from within service directory." unless id 19 | 20 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 21 | version ||= options[:version] 22 | 23 | encoded_name = FastlyCTL::Utils.percent_encode(name) if name 24 | 25 | case action 26 | when "create" 27 | abort "Must specify name for dictionary" unless name 28 | 29 | params = { name: name } 30 | params[:write_only] = true if options.key?(:write_only) 31 | 32 | FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/version/#{version}/dictionary", params: params) 33 | 34 | say("Dictionary #{name} created.") 35 | when "delete" 36 | abort "Must specify name for dictionary" unless name 37 | FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}") 38 | 39 | say("Dictionary #{name} deleted.") 40 | when "list" 41 | resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary") 42 | 43 | say("No dictionaries on service in this version.") unless resp.length > 0 44 | 45 | resp.each do |d| 46 | puts "#{d["id"]} - #{d["name"]}" 47 | end 48 | when "upsert" 49 | abort "Must specify name for dictionary" unless name 50 | abort "Must specify key and value for dictionary item" unless (key && value) 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/#{FastlyCTL::Utils.percent_encode(key)}", params: { item_value: value }) 53 | 54 | say("Dictionary item #{key} set to #{value}.") 55 | when "remove" 56 | abort "Must specify name for dictionary" unless name 57 | abort "Must specify key for dictionary item" unless key 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/#{FastlyCTL::Utils.percent_encode(key)}") 60 | 61 | say("Item #{key} removed from dictionary #{name}.") 62 | when "list_items" 63 | abort "Must specify name for dictionary" unless name 64 | dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}") 65 | resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/dictionary/#{dict["id"]}/items") 66 | 67 | say("No items in dictionary.") unless resp.length > 0 68 | resp.each do |i| 69 | puts "#{i["item_key"]} : #{i["item_value"]}" 70 | end 71 | when "sync" 72 | abort "Must specify name for dictionary" unless name 73 | abort "Must supply comma separated list of keys and values as the \"key\" parameter. " unless key 74 | 75 | pairs = {} 76 | key.split(',').to_set.to_a.each do |kv| 77 | kv = kv.split("=") if kv.include?("=") 78 | kv = kv.split(":") if kv.include?(":") 79 | abort "Keys and values must be separated by an = or : symbol. Found \"#{kv}\"" unless kv.is_a?(Array) 80 | pairs[kv[0]] = kv[1] 81 | end 82 | item_ids = Hash.new 83 | bulk = [] 84 | 85 | dictionary = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}") 86 | items = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/dictionary/#{dictionary["id"]}/items") 87 | items.each do |item| 88 | unless pairs.key?(item["item_key"]) 89 | bulk.push({ 90 | "op" => "delete", 91 | "item_key" => item["item_key"] 92 | }) 93 | next 94 | end 95 | 96 | if (pairs[item["item_key"]] != item["item_value"]) 97 | bulk.push({ 98 | "op": "upsert", 99 | "item_key": item["item_key"], 100 | "item_value": item["item_value"] 101 | }) 102 | end 103 | 104 | pairs.delete(item["item_key"]) 105 | end 106 | 107 | pairs.each do |k,v| 108 | bulk.push({ 109 | "op": "create", 110 | "item_key": k, 111 | "item_value": v 112 | }) 113 | end 114 | 115 | FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/dictionary/#{dictionary["id"]}/items", {body: {items: bulk}.to_json, headers: {"Content-Type" => "application/json"}}) 116 | 117 | say("Sync operation completed successfully with #{bulk.length} operations.") 118 | 119 | when "bulk_add" 120 | abort "Must specify name for dictionary" unless name 121 | 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 122 | dict = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/dictionary/#{encoded_name}") 123 | FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/dictionary/#{dict["id"]}/items", {body: key, headers: {"Content-Type" => "application/json"}}) 124 | 125 | say("Bulk add operation completed successfully.") 126 | else 127 | abort "#{action} is not a valid command" 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/fastlyctl/commands/acl.rb: -------------------------------------------------------------------------------- 1 | module FastlyCTL 2 | class CLI < Thor 3 | desc "acl ACTION ACL_NAME IP", "Manipulate ACLS.\n Actions:\n create: Create an ACL\n 4 | delete: Delete an ACL\n 5 | list: Provide a list of ACLs on this service\n 6 | add: Add an IP/subnet to an ACL\n 7 | remove: Remove an IP/subnet from an ACL\n 8 | list_ips: List all IPs/subnets in the ACL\n 9 | sync: Synchronizes an ACL with a comma separated list of IPs. Will create or delete ACL entries as needed. 10 | bulk_add: Perform operations on the ACL in bulk. A list of operations in JSON format should be specified in the ip field. Documentation on this format can be found here: https://docs.fastly.com/api/config#acl_entry_c352ca5aee49b7898535cce488e3ba82" 11 | method_option :service, :aliases => ["--s"] 12 | method_option :version, :aliases => ["--v"] 13 | method_option :negate, :aliases => ["--n"] 14 | def acl(action, name=false, ip=false) 15 | id = FastlyCTL::Utils.parse_directory unless options[:service] 16 | id ||= options[:service] 17 | 18 | abort "Could not parse service id from directory. Specify service id with --service or use from within service directory." unless id 19 | 20 | version = FastlyCTL::Fetcher.get_writable_version(id) unless options[:version] 21 | version ||= options[:version] 22 | 23 | encoded_name = FastlyCTL::Utils.percent_encode(name) if name 24 | 25 | case action 26 | when "create" 27 | abort "Must specify name for ACL" unless name 28 | FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/version/#{version}/acl", params: { name: name }) 29 | 30 | say("ACL #{name} created.") 31 | when "delete" 32 | abort "Must specify name for ACL" unless name 33 | FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/version/#{version}/acl/#{encoded_name}") 34 | 35 | say("ACL #{name} deleted.") 36 | when "list" 37 | resp = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl") 38 | 39 | say("No ACLs on service in this version.") unless resp.length > 0 40 | 41 | resp.each do |d| 42 | puts "#{d["id"]} - #{d["name"]}" 43 | end 44 | when "add" 45 | abort "Must specify name for ACL" unless name 46 | abort "Must specify IP" unless ip 47 | 48 | subnet = false 49 | if ip.include?("/") 50 | ip = ip.sub(/\/(\d{1,2})/,"") 51 | subnet = $1 52 | end 53 | 54 | acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}") 55 | 56 | params = { 57 | ip: ip, 58 | negated: options.key?(:negate) ? "1" : "0" 59 | } 60 | params[:subnet] = subnet if subnet 61 | 62 | FastlyCTL::Fetcher.api_request(:post, "/service/#{id}/acl/#{acl["id"]}/entry", params: params) 63 | 64 | say("#{ip} added to ACL #{name}.") 65 | when "remove" 66 | abort "Must specify name for ACL" unless name 67 | abort "Must specify IP for ACL entry" unless ip 68 | acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}") 69 | entries = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/acl/#{acl["id"]}/entries") 70 | 71 | entry = false 72 | entries.each do |e| 73 | if e["ip"] == ip 74 | entry = e 75 | break 76 | end 77 | end 78 | 79 | abort "IP #{ip} not found in ACL" unless entry 80 | 81 | FastlyCTL::Fetcher.api_request(:delete, "/service/#{id}/acl/#{acl["id"]}/entry/#{entry["id"]}") 82 | 83 | say("IP #{ip} removed from ACL #{name}.") 84 | when "list_ips" 85 | abort "Must specify name for ACL" unless name 86 | acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}") 87 | entries = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/acl/#{acl["id"]}/entries") 88 | 89 | say("No items in ACL.") unless entries.length > 0 90 | entries.each do |i| 91 | puts "#{i["ip"]}#{i["subnet"].nil? ? "" : "/"+i["subnet"].to_s} - Negated: #{i["negated"] == "0" ? "false" : "true"}" 92 | end 93 | when "sync" 94 | abort "Must specify name for ACL" unless name 95 | abort "Must supply comma separated list of IPs as the \"ip\" parameter" unless ip 96 | 97 | ips = ip.split(',').to_set.to_a 98 | entry_ids = Hash.new 99 | current_ips = [] 100 | 101 | acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}") 102 | entries = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/acl/#{acl["id"]}/entries") 103 | entries.each do |entry| 104 | ip_with_subnet = entry["ip"] 105 | ip_with_subnet += "/" + entry["subnet"].to_s if (entry.key?("subnet") && !entry["subnet"].nil?) 106 | 107 | entry_ids[ip_with_subnet] = entry["id"] 108 | current_ips.push(ip_with_subnet) 109 | end 110 | 111 | to_add = ips - current_ips 112 | to_remove = current_ips - ips 113 | 114 | bulk = [] 115 | 116 | to_add.each do |add| 117 | subnet = false 118 | if add.include?("/") 119 | add = add.sub(/\/(\d{1,2})/,"") 120 | subnet = $1 121 | end 122 | 123 | params = { 124 | "op" => "create", 125 | "ip" => add 126 | } 127 | params["subnet"] = subnet if subnet 128 | 129 | bulk.push(params) 130 | end 131 | 132 | to_remove.each do |remove| 133 | entry_id = entry_ids[remove] 134 | remove = remove.sub(/\/(\d{1,2})/,"") if remove.include?("/") 135 | 136 | bulk.push({ 137 | "op" => "delete", 138 | "id" => entry_id 139 | }) 140 | end 141 | 142 | FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/acl/#{acl["id"]}/entries", {body: {entries: bulk}.to_json, headers: {"Content-Type" => "application/json"}}) 143 | 144 | say("Sync operation completed successfully with #{bulk.length} operations.") 145 | 146 | when "bulk_add" 147 | abort "Must specify name for ACL" unless name 148 | 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 149 | acl = FastlyCTL::Fetcher.api_request(:get, "/service/#{id}/version/#{version}/acl/#{encoded_name}") 150 | FastlyCTL::Fetcher.api_request(:patch, "/service/#{id}/acl/#{acl["id"]}/entries", {body: ip, headers: {"Content-Type" => "application/json"}}) 151 | 152 | say("Bulk add operation completed successfully.") 153 | else 154 | abort "#{action} is not a valid command" 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/fastlyctl/fetcher.rb: -------------------------------------------------------------------------------- 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[:disable_token] ||= false 9 | options[:expected_responses] ||= [200] 10 | options[:use_vnd] ||= false 11 | 12 | headers = {"Accept" => "application/json", "Connection" => "close", "User-Agent" => "FastlyCTL: https://github.com/fastly/fastlyctl"} 13 | 14 | if options[:endpoint] == :app 15 | headers["Referer"] = FastlyCTL::FASTLY_APP 16 | headers["Fastly-API-Request"] = "true" 17 | end 18 | 19 | if FastlyCTL::Token && !options[:disable_token] 20 | headers["Fastly-Key"] = FastlyCTL::Token 21 | end 22 | 23 | headers["Content-Type"] = "application/x-www-form-urlencoded" if (method == :post || method == :put) 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 | 34 | headers.merge!(options[:headers]) if options[:headers].count > 0 35 | 36 | # dont allow header splitting on anything 37 | headers.each do |k,v| 38 | headers[k] = v.gsub(/\r|\n/,'') 39 | end 40 | 41 | url = "#{options[:endpoint] == :api ? FastlyCTL::FASTLY_API : FastlyCTL::FASTLY_RT_API}#{path}" 42 | 43 | response = Typhoeus::Request.new( 44 | url, 45 | method: method, 46 | params: options[:params], 47 | headers: headers, 48 | body: options[:body] 49 | ).run 50 | 51 | if !options[:expected_responses].include?(response.response_code) 52 | case 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}." 65 | end 66 | 67 | error += " Method: #{method.to_s.upcase}, Path: #{path}\n" 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 83 | 84 | abort error 85 | end 86 | 87 | return response.response_body unless (response.headers["Content-Type"] =~ /json$/) 88 | 89 | if response.response_body.length > 1 90 | begin 91 | return JSON.parse(response.response_body) 92 | rescue JSON::ParserError 93 | abort "Failed to parse JSON response from Fastly API" 94 | end 95 | else 96 | return {} 97 | end 98 | end 99 | 100 | def self.domain_to_service_id(domain) 101 | response = Typhoeus::Request.new(FastlyCTL::FASTLY_APP, method:"FASTLYSERVICEMATCH", headers: { :host => domain}).run 102 | 103 | abort "Failed to fetch Fastly service ID or service ID does not exist" if response.response_code != 204 104 | 105 | abort "Fastly response did not contain service ID" unless response.headers["Fastly-Service-Id"] 106 | 107 | return response.headers["Fastly-Service-Id"] 108 | end 109 | 110 | def self.get_active_version(id) 111 | service = self.api_request(:get, "/service/#{id}") 112 | 113 | max = 1 114 | 115 | service["versions"].each do |v| 116 | if v["active"] == true 117 | return v["number"] 118 | end 119 | 120 | max = v["number"] if v["number"] > max 121 | end 122 | 123 | return max 124 | end 125 | 126 | def self.get_writable_version(id) 127 | service = self.api_request(:get, "/service/#{id}") 128 | 129 | active = false 130 | version = false 131 | max = 1 132 | service["versions"].each do |v| 133 | if v["active"] == true 134 | active = v["number"].to_i 135 | end 136 | 137 | if active && v["number"].to_i > active && v["locked"] == false 138 | version = v["number"] 139 | end 140 | 141 | max = version if version && version > max 142 | end 143 | 144 | return max unless active 145 | 146 | version = self.api_request(:put, "/service/#{id}/version/#{active}/clone")["number"] unless version 147 | 148 | return version 149 | end 150 | 151 | def self.get_vcl(id, version, generated=false) 152 | if generated 153 | vcl = self.api_request(:get, "/service/#{id}/version/#{version}/generated_vcl") 154 | else 155 | vcl = self.api_request(:get, "/service/#{id}/version/#{version}/vcl?include_content=1") 156 | end 157 | 158 | if vcl.length == 0 159 | return false 160 | else 161 | return vcl 162 | end 163 | end 164 | 165 | def self.get_snippets(id,version) 166 | snippet = self.api_request(:get, "/service/#{id}/version/#{version}/snippet") 167 | 168 | if snippet.length == 0 169 | return false 170 | else 171 | return snippet 172 | end 173 | end 174 | 175 | def self.upload_snippet(service,version,content,name) 176 | return FastlyCTL::Fetcher.api_request(:put, "/service/#{service}/version/#{version}/snippet/#{FastlyCTL::Utils.percent_encode(name)}", {:endpoint => :api, body: { 177 | content: content 178 | } 179 | }) 180 | end 181 | 182 | def self.upload_vcl(service,version,content,name,is_main=true,is_new=false) 183 | params = { name: name, main: "#{is_main ? "1" : "0"}", content: content } 184 | 185 | # try to create, if that fails, update 186 | if is_new 187 | response = FastlyCTL::Fetcher.api_request(:post, "/service/#{service}/version/#{version}/vcl", {:endpoint => :api, body: params, expected_responses:[200,409]}) 188 | if response["msg"] != "Duplicate record" 189 | return 190 | end 191 | end 192 | 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]}) 194 | 195 | # The VCL got deleted so recreate it. 196 | if response["msg"] == "Record not found" 197 | FastlyCTL::Fetcher.api_request(:post, "/service/#{service}/version/#{version}/vcl", {:endpoint => :api, body: params}) 198 | end 199 | end 200 | 201 | def self.create_token(options) 202 | thor = Thor::Shell::Basic.new 203 | 204 | headers = {} 205 | resp = FastlyCTL::Fetcher.api_request(:post, "/tokens", { 206 | disable_token: true, 207 | endpoint: :api, 208 | body: options, 209 | headers: headers, 210 | expected_responses: [200,400] 211 | }) 212 | 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) 216 | 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 228 | 229 | thor.say("\n#{resp["id"]} created.") 230 | 231 | return resp 232 | end 233 | end 234 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastlyCTL [![Gem Version](https://img.shields.io/gem/v/fastlyctl.svg)](https://rubygems.org/gems/fastlyctl) 2 | 3 | **DEPRECATED**: FastlyCTL is no longer maintained or supported. Many features are already available in the official [Fastly CLI](https://github.com/fastly/cli), which is actively maintained and developed. Please consider migrating if possible. 4 | 5 | --- 6 | 7 | A CLI for managing Fastly configurations with [Fastly's API](https://docs.fastly.com/api/config). 8 | 9 | ## Dependencies 10 | 11 | * Ruby 2.2+ 12 | * [diffy](https://rubygems.org/gems/diffy) 13 | * [launchy](https://rubygems.org/gems/launchy) 14 | * [thor](https://rubygems.org/gems/thor) 15 | * [typhoeus](https://rubygems.org/gems/typhoeus) 16 | 17 | ## Installation 18 | 19 | ``` 20 | gem install fastlyctl 21 | ``` 22 | 23 | ## Workflow 24 | 25 | Basic setup for a service: 26 | 27 | ``` 28 | $ fastlyctl download --service 72rdJo8ipqaHRFYnn12G2q 29 | No VCLs on this service, however a folder has been created. Create VCLs in this folder and upload. 30 | $ cd Sandbox\ -\ 72rdJo8ipqaHRFYnn12G2q/ 31 | $ fastlyctl skeleton 32 | Boilerplate written to main.vcl. 33 | $ fastlyctl upload 34 | VCL main does not currently exist on the service, would you like to create it? y 35 | [You will see a diff here for the new VCL] 36 | Given the above diff, are you sure you want to upload your changes? y 37 | main uploaded to 72rdJo8ipqaHRFYnn12G2q 38 | VCL(s) have been uploaded to version 286 and validated. 39 | $ fastlyctl activate 40 | Version 286 on 72rdJo8ipqaHRFYnn12G2q activated. 41 | ``` 42 | 43 | Once you are past this point you can edit your VCLs and use the commmand `fastlyctl upload && fastlyctl activate`. The service ID will be automatically inferred from the folder you are currently in. In fact, all commands will attempt to assume the service ID of the current directory if it is relevant. 44 | 45 | You may find it useful to keep a Github repo with one folder created by this command for each service. This way you can version your VCL files. 46 | 47 | ## Command Reference 48 | 49 | ### acl 50 | 51 | Manipulate ACLs on a service. 52 | 53 | Usage: 54 | 55 | ``` 56 | fastlyctl acl [action] [acl_name] [ip] 57 | ``` 58 | 59 | Available Actions: 60 | * create: Creates a new ACL. `ip` parameter is omitted. 61 | * delete: Deletes an ACL. `ip` parameter is omitted. 62 | * list: Lists all ACLs. `ip` parameter is omitted. 63 | * add: Adds a new IP or Subnet to an ACL. 64 | * remove: Removes an IP or Subnet from an ACL. 65 | * list_ips: Lists all IPs/Subnets in an ACL. 66 | * sync: Synchronizes an ACL with a comma separated list of IPs. Will create or delete ACL entries as needed. 67 | * bulk_add: Adds multiple items to an ACL. See [this documentation](https://docs.fastly.com/api/config#acl_entry_c352ca5aee49b7898535cce488e3ba82) for information on the format. 68 | 69 | Flags: 70 | * --s: The service ID to use. Current working directory is assumed. 71 | * --v: The version to use. Latest writable version is assumed. 72 | 73 | ### activate 74 | 75 | Activates a service version. 76 | 77 | Usage: 78 | 79 | ``` 80 | fastlyctl activate 81 | ``` 82 | 83 | Flags: 84 | * --s: The service ID to activate. Current working directory is assumed. 85 | * --v: The version to activate. Latest writable version is assumed. 86 | * --c: Adds a comment to the version. 87 | 88 | ### clone 89 | 90 | Clones a service version to a new version on another service. 91 | 92 | Usage: 93 | 94 | ``` 95 | fastlyctl clone [sid_1] [sid_2] 96 | ``` 97 | 98 | Flags 99 | * --v: The version to clone. The currently active version is assumed. 100 | * --sl: Skip logging objects during the clone. 101 | 102 | ### copy 103 | 104 | Copies an object from one service to another 105 | 106 | Usage: 107 | 108 | ``` 109 | fastlyctl copy [sid_1] [sid_2] [obj_type] [obj_name] 110 | ``` 111 | 112 | Flags 113 | * --v1: The version to clone from on the source service. The currently active version is assumed. 114 | * --v2: The version to clone to on the target service. Latest writable version is assumed. 115 | 116 | ### create_service 117 | 118 | Creates a new service. 119 | 120 | Usage: 121 | 122 | ``` 123 | fastlyctl create_service [name] 124 | ``` 125 | 126 | ### dictionary 127 | 128 | Manipulate edge dictionaries on a service. 129 | 130 | Usage: 131 | 132 | ``` 133 | fastlyctl dictionary [action] [dictionary_name] [key] [value] 134 | ``` 135 | 136 | Available Actions: 137 | * create: Creates a new dictionary. Key and value parameters are omitted. 138 | * delete: Deletes a dictionary. Key and value parameters are omitted. 139 | * list: Lists all dictionaries. Key and value parameters are omitted. 140 | * upsert: Inserts a new item into a dictionary. If the item exists, its value will be updated. 141 | * remove: Removes an item from a dictionary. 142 | * list_items: Lists all items in a dictionary. 143 | * sync: Synchronizes a dictionary with a comma separated list of key/value pairs. Will create, delete, or update keys as needed. Separate keys and values with `=` or `:`. 144 | * bulk_add: Adds multiple items to a dictionary. See [this documentation](https://docs.fastly.com/api/config#dictionary_item_dc826ce1255a7c42bc48eb204eed8f7f) for information on the format. 145 | 146 | Flags: 147 | * --s: The service ID to use. Current working directory is assumed. 148 | * --v: The version to use. Latest writable version is assumed. 149 | * --wo: When used with `create`, flags the dictionary as write-only. 150 | 151 | ### diff 152 | 153 | Provides a diff of two service versions. You may optionally specify which two service IDs and which two versions to diff. If you do not provide service IDs, the context of the current working directory is assumed. 154 | 155 | * If you provide no service IDs, the service ID of the working directory is assumed. 156 | * If you do not specify versions, active VCL will be diffed with local VCL in the current directory. 157 | * If you specify version 1 but not version 2, version 1 will be diffed with local VCL 158 | * If you specify both versions, they will be diffed with each other. 159 | * If you provide service 1 but not service 2, service 2 will be assumed from the current working directory. 160 | * Regardless of how you specify services, if service 1 and service 2 are _different_, the versions will default to the active versions instead of local VCL. 161 | 162 | Usage: 163 | 164 | ``` 165 | fastlyctl diff 166 | ``` 167 | 168 | * --s1: The first service to diff against. 169 | * --v1: The version to diff. 170 | * --s2: The second service to diff against. 171 | * --v2: The second service's version to diff. 172 | * --g: Diffs the generated VCL instead of the custom VCL. 173 | 174 | ### domain 175 | 176 | Manipulate domains on a service. 177 | 178 | Usage: 179 | 180 | ``` 181 | fastlyctl domain [action] [hostname] 182 | ``` 183 | 184 | Available Actions: 185 | * create: Create a new domain. 186 | * delete: Delete a domain. 187 | * list: List all domains. 188 | * check: Check the DNS of all domains on a service and print the status. 189 | 190 | Flags: 191 | * --s: The service ID to use. Current working directory is assumed. 192 | * --v: The version to use. Latest writable version is assumed. 193 | 194 | ### download 195 | 196 | Download the VCLs and snippets on a service. If you are not in a service directory already, a new directory will be created. 197 | 198 | Usage: 199 | 200 | ``` 201 | fastlyctl download 202 | ``` 203 | 204 | Flags: 205 | * --s: The service ID to download. Current working directory is assumed. 206 | * --v: The version to download. The currently active version is assumed. 207 | 208 | ### logging 209 | 210 | Manage the realtime logging configuration for a service, as well as checking on the status of the logging endpoints. Logging requires a subcommand of either `status` or the name of a logging provider listed below. 211 | 212 | ##### status 213 | 214 | Status returns the current status of your logging configurations in JSON format. Helps in telling you whether or not the logging is working or returning an error. 215 | 216 | Usage: 217 | ``` 218 | fastlyctl logging status 219 | ``` 220 | 221 | Flags: 222 | 223 | * `--s / --service` Service ID to use 224 | 225 | ##### BigQuery 226 | 227 | Usage: 228 | ``` 229 | fastlyctl logging bigquery ACTION [FLAGS] 230 | ``` 231 | 232 | Supported ACTIONs are `create`, `update`, `show`, `delete`, `list` 233 | 234 | Flags: 235 | 236 | * `--s / --service` Service ID to use (required) 237 | * `--v / --version` Version of the service to use 238 | * `--n / --name` Current name of the logging configuration 239 | * `--nn / --new-name` Used for the update method to rename a configuration 240 | * `--ff / --format-file` Path to the file containing the JSON Representation of the logline, must match BigQuery schema 241 | * `--u / --user` Google Cloud Service Account Email 242 | * `--scf / --secret-key-file` Path to the file that contains the Google Cloud Account secret key 243 | * `--p / --project-id` Google Cloud Project ID 244 | * `--d / --dataset` Google BigQuery dataset 245 | * `--t / --table` Google BigQuery table 246 | * `--ts / --template-suffix` Google table name suffix 247 | * `--pl / --placement` Placement of the logging call, can be none or waf_debug. Not required and no default 248 | * `--r / --response-condition` When to execute, if empty it is always 249 | 250 | 251 | To print the full list of the options required type the command: 252 | 253 | ``` 254 | fastlyctl logging bigquery --help 255 | ``` 256 | 257 | 258 | ### login 259 | 260 | Login to the Fastly app and create an API token. This token will be stored in your home directory for the CLI to use for all requests. 261 | 262 | If your origanization uses SSO to login to Fastly, this command will prompt you to create a token and save it to `~/.fastlyctl_token` on your computer. You may also create the token and save it to the fastlyctl_token file without using the `fastlyctl login` command at all. 263 | 264 | Usage: 265 | 266 | ``` 267 | fastlyctl login 268 | ``` 269 | 270 | ### open 271 | 272 | Opens the Fastly app for a service for a hostname of a service ID. 273 | 274 | Usage: 275 | 276 | ``` 277 | fastlyctl open [hostname] 278 | ``` 279 | 280 | Flags: 281 | * --s: The service ID to open. Current working directory is assumed. 282 | 283 | ### purge_all 284 | 285 | Perform a purge all on a service. 286 | 287 | Usage: 288 | 289 | ``` 290 | fastlyctl purge_all 291 | ``` 292 | Flags: 293 | * --s: The service ID to purge. Current working directory is assumed. 294 | 295 | ### skeleton 296 | 297 | Download the VCL boilerplate into the current directory. 298 | 299 | Usage 300 | 301 | ``` 302 | fastlyctl skeleton [local_filename] 303 | ``` 304 | 305 | ### snippet 306 | 307 | Manipulate snippets on a service. 308 | 309 | Usage: 310 | 311 | ``` 312 | fastlyctl snippet [action] [snippet_name] 313 | ``` 314 | 315 | Available Actions: 316 | * create: Create a new snippet 317 | * upload: Upload a specific dynamic snippet 318 | * delete: Delete a snippet 319 | * list: List all snippets 320 | 321 | Flags: 322 | * --s: The service ID to use. Current working directory is assumed. 323 | * --v: The version to use. Latest writable version is assumed. 324 | * --t: The type of snippet to create. Types are named after subroutines--for instance a snippet for `vcl_recv` would be of type `recv`. Use `init` for snippets outside of a subroutine. 325 | * --d: When used with the create command, specifies that the snippet should be dynamic. 326 | * --y: Answer yes to all prompts 327 | 328 | ### tls 329 | 330 | #### managed 331 | 332 | Usage: 333 | 334 | ``` 335 | fastlyctl tls managed [subcommand] [domain] 336 | ``` 337 | 338 | Available Subcommands: 339 | * create: Create a Managed TLS Subscription for `[domain]` 340 | * status: Print the status of all Managed TLS Subscriptions 341 | * challenges: Print the challenges available for the verification of a certificate for `[domain]` 342 | * delete: Delete a Managed TLS Subscription for `[domain]` 343 | 344 | ### token 345 | 346 | Manipulate tokens for an account. 347 | 348 | Usage: 349 | 350 | ``` 351 | fastlyctl token [action] 352 | ``` 353 | 354 | Available Actions: 355 | * create: Create a token 356 | * delete: Delete a token 357 | * list: List all tokens on the account 358 | 359 | Flags: 360 | * --scope: Scope of the token. See Fastly's public API documentation for a [list of scopes](https://docs.fastly.com/api/auth#scopes). 361 | * --s: The services to restrict this token to. The token cannot be used to modify any services not on this list if this option is specified. 362 | 363 | ### upload 364 | 365 | Upload VCLs and snippets to a service. 366 | 367 | Usage: 368 | 369 | ``` 370 | fastlyctl upload 371 | ``` 372 | 373 | Flags: 374 | * --v: The version to upload the VCL to. The latest writable version is assumed. 375 | * --c: Adds a comment to the version. 376 | 377 | ### watch 378 | 379 | Watch live stats on a service. 380 | 381 | Usage: 382 | 383 | ``` 384 | fastlyctl watch [pop] 385 | ``` 386 | 387 | Flags: 388 | * --s: The service ID to watch. Current working directory is assumed. 389 | 390 | ## Debug Mode 391 | 392 | The `--debug` flag is available on any command. Using it will cause fastlyctl to print the libcurl output for any requests it makes. 393 | 394 | ## License 395 | 396 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 397 | 398 | ## Publishing 399 | 400 | Bump version in `/lib/fastlyctl/version.rb`. 401 | 402 | ```bash 403 | $ gem build fastlyctl.gemspec 404 | $ gem push fastlyctl-1.0.X.gem 405 | ``` 406 | --------------------------------------------------------------------------------