Skip to content

Commit da55c3f

Browse files
authored
Merge pull request #270 from 3scale/product-policies
policies command, import and export
2 parents 24b8540 + 4ca08e6 commit da55c3f

8 files changed

Lines changed: 356 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* List, Show, Promote, Export [Proxy Configuration](docs/proxy-config.md)
2525
* [Copy Policy Registry](docs/copy-policy-registry.md)
2626
* Create, Apply, List, Show, Delete, Suspend, Resume [Applications](docs/applications.md)
27+
* [Export/Import Product Policy Chain](docs/export-import-policy-chain.md)
2728
* [Remotes](docs/remotes.md)
2829
* [Development](#development)
2930
* [Testing](#testing)

docs/export-import-policy-chain.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
## Export/Import Product Policy Chain
2+
3+
Common description:
4+
* Export/Import one product's policy chain to/from yaml/json content.
5+
* Product can be referenced by `id` or `system_name`.
6+
7+
Specific to `export` command:
8+
* Read only operation on remote product.
9+
* Command `output` can be `stdout` or file. If not specified by `-f` option, by default, content will be written on `stdout`.
10+
* Command output format can be `json` or `yaml` using `-o` option. Defaults to `yaml`
11+
12+
Specific to `import` command:
13+
* Imported content can be `stdin` (by default), file (`-f` option) or URL (`-u` option).
14+
* Imported content can be `yaml` or `json`. No need to specify the format, the content format will be detected automatically.
15+
* Imported resource as the source of truth. The existing policy chain will be updated with the imported new one. `SET` semantics implemented.
16+
* All content validation is delegated to the 3scale API.
17+
18+
### Export Product Policy Chain
19+
20+
```shell
21+
NAME
22+
export - export product policy chain
23+
24+
USAGE
25+
3scale policies export [opts] <remote>
26+
<product>
27+
28+
DESCRIPTION
29+
export product policy chain
30+
31+
OPTIONS
32+
-f --file=<value> Write to file instead of stdout
33+
-o --output=<value> Output format. One of: json|yaml
34+
```
35+
36+
### Import Product Policy Chain
37+
38+
```shell
39+
NAME
40+
import - import product policy chain
41+
42+
USAGE
43+
3scale policies import [opts] <remote>
44+
<product>
45+
46+
DESCRIPTION
47+
import product policy chain
48+
49+
OPTIONS
50+
-f --file=<value> Read from file
51+
-u --url=<value> Read from url
52+
```
53+
54+
### Export policy chain to a file in yaml
55+
56+
```shell
57+
$ 3scale policies export -f policies.yaml -o yaml remote_name product_name
58+
```
59+
60+
### Import policy chain from a file
61+
62+
```shell
63+
$ 3scale policies import -f plan.yaml remote_name product_name
64+
```
65+
66+
### Import policy chain from URI
67+
68+
```shell
69+
$ 3scale policies import -f http[s]://domain/resource/path.yaml remote_name product_name
70+
```

lib/3scale_toolbox/commands.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
require '3scale_toolbox/commands/application_command'
1616
require '3scale_toolbox/commands/backend_command'
1717
require '3scale_toolbox/commands/product_command'
18+
require '3scale_toolbox/commands/policies_command'
1819

1920
module ThreeScaleToolbox
2021
module Commands
@@ -35,6 +36,7 @@ module Commands
3536
ThreeScaleToolbox::Commands::ApplicationCommand,
3637
ThreeScaleToolbox::Commands::BackendCommand,
3738
ThreeScaleToolbox::Commands::ProductCommand,
39+
ThreeScaleToolbox::Commands::PoliciesCommand,
3840
].freeze
3941
end
4042
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require '3scale_toolbox/commands/policies_command/export_command'
2+
require '3scale_toolbox/commands/policies_command/import_command'
3+
4+
module ThreeScaleToolbox
5+
module Commands
6+
module PoliciesCommand
7+
include ThreeScaleToolbox::Command
8+
def self.command
9+
Cri::Command.define do
10+
name 'policies'
11+
usage 'policies <sub-command> [options]'
12+
summary 'product super command'
13+
description 'Policies commands'
14+
15+
run do |_opts, _args, cmd|
16+
puts cmd.help
17+
end
18+
end
19+
end
20+
add_subcommand(ExportSubcommand)
21+
add_subcommand(ImportSubcommand)
22+
end
23+
end
24+
end
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
module ThreeScaleToolbox
2+
module Commands
3+
module PoliciesCommand
4+
class ExportSubcommand < Cri::CommandRunner
5+
include ThreeScaleToolbox::Command
6+
7+
class JSONSerializer
8+
def call(object)
9+
JSON.pretty_generate(object)
10+
end
11+
end
12+
13+
class YAMLSerializer
14+
def call(object)
15+
YAML.dump(object)
16+
end
17+
end
18+
19+
class SerializerTransformer
20+
def call(output_format)
21+
raise unless %w[yaml json].include?(output_format)
22+
23+
case output_format
24+
when 'yaml'
25+
YAMLSerializer.new
26+
when 'json'
27+
JSONSerializer.new
28+
end
29+
end
30+
end
31+
32+
def self.command
33+
Cri::Command.define do
34+
name 'export'
35+
usage 'export [opts] <remote> <product>'
36+
summary 'export product policy chain'
37+
description 'export product policy chain'
38+
39+
option :f, :file, 'Write to file instead of stdout', argument: :required
40+
option :o, :output, 'Output format. One of: json|yaml', argument: :required, transform: SerializerTransformer.new
41+
param :remote
42+
param :service_ref
43+
44+
runner ExportSubcommand
45+
end
46+
end
47+
48+
def run
49+
select_output do |output|
50+
output.write(serializer.call(product.policies))
51+
end
52+
end
53+
54+
private
55+
56+
def remote
57+
@remote ||= threescale_client(arguments[:remote])
58+
end
59+
60+
def product
61+
@product ||= find_product
62+
end
63+
64+
def service_ref
65+
arguments[:service_ref]
66+
end
67+
68+
def find_product
69+
Entities::Service.find(remote: remote,
70+
ref: service_ref).tap do |svc|
71+
raise ThreeScaleToolbox::Error, "Product #{service_ref} does not exist" if svc.nil?
72+
end
73+
end
74+
75+
def file
76+
options[:file]
77+
end
78+
79+
def select_output
80+
ios = if file
81+
File.open(file, 'w')
82+
else
83+
$stdout
84+
end
85+
begin
86+
yield(ios)
87+
ensure
88+
ios.close
89+
end
90+
end
91+
92+
def serializer
93+
options.fetch(:output, YAMLSerializer.new)
94+
end
95+
end
96+
end
97+
end
98+
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module ThreeScaleToolbox
2+
module Commands
3+
module PoliciesCommand
4+
class ImportSubcommand < Cri::CommandRunner
5+
include ThreeScaleToolbox::Command
6+
include ThreeScaleToolbox::ResourceReader
7+
8+
def self.command
9+
Cri::Command.define do
10+
name 'import'
11+
usage 'import [opts] <remote> <product>'
12+
summary 'import product policy chain'
13+
description 'import product policy chain'
14+
15+
option :f, :file, 'Read from file', argument: :required
16+
option :u, :url, 'Read from url', argument: :required
17+
param :remote
18+
param :service_ref
19+
20+
runner ImportSubcommand
21+
end
22+
end
23+
24+
def run
25+
res = product.update_policies('policies_config' => policies)
26+
if res.is_a?(Hash) && (errors = res['errors'])
27+
raise ThreeScaleToolbox::Error, "Product policies have not been imported. #{errors}"
28+
end
29+
if res.is_a?(Array) && (error_item = res.find { |i| i.key?('errors') })
30+
raise ThreeScaleToolbox::Error, "Product policies have not been imported. #{error_item['errors']}"
31+
end
32+
end
33+
34+
private
35+
36+
def remote
37+
@remote ||= threescale_client(arguments[:remote])
38+
end
39+
40+
def service_ref
41+
arguments[:service_ref]
42+
end
43+
44+
def product
45+
@product ||= find_product
46+
end
47+
48+
def find_product
49+
Entities::Service.find(remote: remote,
50+
ref: service_ref).tap do |svc|
51+
raise ThreeScaleToolbox::Error, "Product #{service_ref} does not exist" if svc.nil?
52+
end
53+
end
54+
55+
def policies
56+
@policies ||= load_resource(options[:file] || options[:url] || '-')
57+
end
58+
end
59+
end
60+
end
61+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
RSpec.describe ThreeScaleToolbox::Commands::PoliciesCommand::ExportSubcommand do
2+
include_context :temp_dir
3+
4+
context '#run' do
5+
let(:remote) { instance_double(ThreeScale::API::Client) }
6+
let(:remote_name) { 'myremote' }
7+
let(:service_ref) { 1 }
8+
let(:arguments) { { remote: remote_name, service_ref: service_ref.to_s } }
9+
let(:output_file) {}
10+
let(:options) { { file: output_file } }
11+
let(:policy1) do
12+
{
13+
'name' => 'apicast',
14+
'version' => 'builtin',
15+
'configuration' => {},
16+
'enabled' => true
17+
}
18+
end
19+
let(:policy2) do
20+
{
21+
'name' => 'content_caching',
22+
'version' => 'builtin',
23+
'configuration' => {},
24+
'enabled' => true
25+
}
26+
end
27+
let(:policy_chain) { [policy1, policy2] }
28+
let(:svc_a_attrs) { { 'id' => service_ref } }
29+
30+
subject { described_class.new(options, arguments, nil) }
31+
32+
before :example do
33+
expect(subject).to receive(:threescale_client).with(remote_name).and_return(remote)
34+
expect(remote).to receive(:show_service).and_return(svc_a_attrs)
35+
expect(remote).to receive(:show_policies).and_return(policy_chain)
36+
end
37+
38+
context 'when file selected' do
39+
let(:output_file) { tmp_dir.join('policies.yaml') }
40+
41+
it 'content is written to the file' do
42+
subject.run
43+
expect(output_file.read.size).to be_positive
44+
end
45+
46+
it 'exports product policy chain' do
47+
subject.run
48+
exported_policies = YAML.safe_load(output_file.read)
49+
expect(exported_policies).to include(policy1, policy2)
50+
end
51+
end
52+
end
53+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
RSpec.describe ThreeScaleToolbox::Commands::PoliciesCommand::ImportSubcommand do
2+
include_context :temp_dir
3+
4+
context '#run' do
5+
let(:remote) { instance_double(ThreeScale::API::Client) }
6+
let(:remote_name) { 'myremote' }
7+
let(:service_ref) { 1 }
8+
let(:arguments) { { remote: remote_name, service_ref: service_ref.to_s } }
9+
let(:input_file) {}
10+
let(:options) { { file: input_file } }
11+
let(:policy1) do
12+
{
13+
'name' => 'apicast',
14+
'version' => 'builtin',
15+
'configuration' => {},
16+
'enabled' => true
17+
}
18+
end
19+
let(:policy2) do
20+
{
21+
'name' => 'content_caching',
22+
'version' => 'builtin',
23+
'configuration' => {},
24+
'enabled' => true
25+
}
26+
end
27+
let(:policy_chain) { [policy1, policy2] }
28+
let(:svc_a_attrs) { { 'id' => service_ref } }
29+
30+
subject { described_class.new(options, arguments, nil) }
31+
32+
before :example do
33+
expect(subject).to receive(:threescale_client).with(remote_name).and_return(remote)
34+
expect(remote).to receive(:show_service).and_return(svc_a_attrs)
35+
end
36+
37+
context 'when file selected' do
38+
let(:input_file) { tmp_dir.join('policies.yaml').tap { |policies_file| policies_file.write(policy_chain.to_yaml) } }
39+
40+
it 'imports product policy chain' do
41+
expect(remote).to receive(:update_policies).with(service_ref, hash_including('policies_config' => policy_chain))
42+
43+
subject.run
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)