aptible-cli 0.18.2 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,16 +39,17 @@ module Aptible
39
39
  type: 'restore',
40
40
  handle: handle,
41
41
  container_size: options[:container_size],
42
- disk_size: options[:disk_size] || options[:size],
42
+ disk_size: options[:disk_size],
43
43
  destination_account: destination_account,
44
44
  key_arn: options[:key_arn]
45
45
  }.delete_if { |_, v| v.nil? }
46
46
 
47
- CLI.logger.warn([
48
- 'You have used the "--size" option to specify a disk size.',
49
- 'This option which be deprecated in a future version.',
50
- 'Please use the "--disk-size" option, instead.'
51
- ].join("\n")) if options[:size]
47
+ if options[:size]
48
+ m = 'You have used the "--size" option to specify a disk size.'\
49
+ 'This abiguous option has been removed.'\
50
+ 'Please use the "--disk-size" option, instead.'
51
+ raise Thor::Error, m
52
+ end
52
53
 
53
54
  operation = backup.create_operation!(opts)
54
55
  CLI.logger.info "Restoring backup into #{handle}"
@@ -123,7 +124,14 @@ module Aptible
123
124
 
124
125
  operation = backup.create_operation!(type: 'purge')
125
126
  CLI.logger.info "Purging backup #{backup_id}"
126
- attach_to_operation_logs(operation)
127
+ begin
128
+ attach_to_operation_logs(operation)
129
+ rescue HyperResource::ClientError => e
130
+ # A 404 here means that the operation completed successfully,
131
+ # and was removed faster than attach_to_operation_logs
132
+ # could attach to the logs.
133
+ raise if e.response.status != 404
134
+ end
127
135
  end
128
136
  end
129
137
  end
@@ -59,8 +59,8 @@ module Aptible
59
59
  option :type, type: :string
60
60
  option :version, type: :string
61
61
  option :container_size, type: :numeric
62
- option :size, type: :numeric
63
62
  option :disk_size, default: 10, type: :numeric
63
+ option :size, type: :numeric
64
64
  option :key_arn, type: :string
65
65
  option :environment
66
66
  define_method 'db:create' do |handle|
@@ -69,15 +69,16 @@ module Aptible
69
69
  db_opts = {
70
70
  handle: handle,
71
71
  initial_container_size: options[:container_size],
72
- initial_disk_size: options[:disk_size] || options[:size],
72
+ initial_disk_size: options[:disk_size],
73
73
  current_kms_arn: options[:key_arn]
74
74
  }.delete_if { |_, v| v.nil? }
75
75
 
76
- CLI.logger.warn([
77
- 'You have used the "--size" option to specify a disk size.',
78
- 'This option which be deprecated in a future version.',
79
- 'Please use the "--disk-size" option, instead.'
80
- ].join("\n")) if options[:size]
76
+ if options[:size]
77
+ m = 'You have used the "--size" option to specify a disk size.'\
78
+ 'This abiguous option has been removed.'\
79
+ 'Please use the "--disk-size" option, instead.'
80
+ raise Thor::Error, m
81
+ end
81
82
 
82
83
  type = options[:type]
83
84
  version = options[:version]
@@ -97,7 +98,7 @@ module Aptible
97
98
  op_opts = {
98
99
  type: 'provision',
99
100
  container_size: options[:container_size],
100
- disk_size: options[:disk_size] || options[:size]
101
+ disk_size: options[:disk_size]
101
102
  }.delete_if { |_, v| v.nil? }
102
103
  op = database.create_operation(op_opts)
103
104
 
@@ -156,17 +157,18 @@ module Aptible
156
157
  opts = {
157
158
  environment: options[:environment],
158
159
  container_size: options[:container_size],
159
- size: options[:disk_size] || options[:size],
160
+ size: options[:disk_size],
160
161
  logical: options[:logical],
161
162
  database_image: image || nil,
162
163
  key_arn: options[:key_arn]
163
164
  }.delete_if { |_, v| v.nil? }
164
165
 
165
- CLI.logger.warn([
166
- 'You have used the "--size" option to specify a disk size.',
167
- 'This option which be deprecated in a future version.',
168
- 'Please use the "--disk-size" option, instead.'
169
- ].join("\n")) if options[:size]
166
+ if options[:size]
167
+ m = 'You have used the "--size" option to specify a disk size.'\
168
+ 'This abiguous option has been removed.'\
169
+ 'Please use the "--disk-size" option, instead.'
170
+ raise Thor::Error, m
171
+ end
170
172
 
171
173
  database = replicate_database(source, dest_handle, opts)
172
174
  render_database(database.reload, database.account)
@@ -279,7 +281,7 @@ module Aptible
279
281
  end
280
282
 
281
283
  desc 'db:restart HANDLE ' \
282
- '[--container-size SIZE_MB] [--disk-size SIZE_GB]' \
284
+ '[--container-size SIZE_MB] [--disk-size SIZE_GB] ' \
283
285
  '[--iops IOPS] [--volume-type [gp2, gp3]]',
284
286
  'Restart a database'
285
287
  option :environment
@@ -294,16 +296,17 @@ module Aptible
294
296
  opts = {
295
297
  type: 'restart',
296
298
  container_size: options[:container_size],
297
- disk_size: options[:disk_size] || options[:size],
299
+ disk_size: options[:disk_size],
298
300
  provisioned_iops: options[:iops],
299
301
  ebs_volume_type: options[:volume_type]
300
302
  }.delete_if { |_, v| v.nil? }
301
303
 
302
- CLI.logger.warn([
303
- 'You have used the "--size" option to specify a disk size.',
304
- 'This option which be deprecated in a future version.',
305
- 'Please use the "--disk-size" option, instead.'
306
- ].join("\n")) if options[:size]
304
+ if options[:size]
305
+ m = 'You have used the "--size" option to specify a disk size.'\
306
+ 'This abiguous option has been removed.'\
307
+ 'Please use the "--disk-size" option, instead.'
308
+ raise Thor::Error, m
309
+ end
307
310
 
308
311
  CLI.logger.info "Restarting #{database.handle}..."
309
312
  op = database.create_operation!(opts)
@@ -33,6 +33,22 @@ module Aptible
33
33
  provision_vhost_and_explain(service, vhost)
34
34
  end
35
35
 
36
+ database_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
37
+ database!
38
+ end
39
+
40
+ desc 'endpoints:database:modify --database DATABASE ' \
41
+ 'ENDPOINT_HOSTNAME',
42
+ 'Modify a Database Endpoint'
43
+ database_modify_flags.declare_options(self)
44
+ define_method 'endpoints:database:modify' do |hostname|
45
+ database = ensure_database(options.merge(db: options[:database]))
46
+ vhost = find_vhost(each_service(database), hostname)
47
+ vhost.update!(**database_modify_flags.prepare(database.account,
48
+ options))
49
+ provision_vhost_and_explain(vhost.service, vhost)
50
+ end
51
+
36
52
  tcp_create_flags = Helpers::Vhost::OptionSetBuilder.new do
37
53
  app!
38
54
  create!
@@ -0,0 +1,159 @@
1
+ module Aptible
2
+ module CLI
3
+ module Subcommands
4
+ module LogDrain
5
+ def self.included(thor)
6
+ thor.class_eval do
7
+ include Helpers::Token
8
+ include Helpers::Database
9
+ include Helpers::LogDrain
10
+
11
+ drain_flags = '--environment ENVIRONMENT ' \
12
+ '[--drain-apps true/false] ' \
13
+ '[--drain_databases true/false] ' \
14
+ '[--drain_ephemeral_sessions true/false] ' \
15
+ '[--drain_proxies true/false]'
16
+
17
+ def self.drain_options
18
+ option :drain_apps, default: true, type: :boolean
19
+ option :drain_databases, default: true, type: :boolean
20
+ option :drain_ephemeral_sessions, default: true, type: :boolean
21
+ option :drain_proxies, default: true, type: :boolean
22
+ option :environment
23
+ end
24
+
25
+ desc 'log_drain:list', 'List all Log Drains'
26
+ option :environment
27
+ define_method 'log_drain:list' do
28
+ Formatter.render(Renderer.current) do |root|
29
+ root.grouped_keyed_list(
30
+ { 'environment' => 'handle' },
31
+ 'handle'
32
+ ) do |node|
33
+ scoped_environments(options).each do |account|
34
+ account.log_drains.each do |drain|
35
+ node.object do |n|
36
+ ResourceFormatter.inject_log_drain(n, drain, account)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ desc 'log_drain:create:elasticsearch HANDLE '\
45
+ '--db DATABASE_HANDLE ' \
46
+ + drain_flags,
47
+ 'Create an Elasticsearch Log Drain'
48
+ drain_options
49
+ option :db, type: :string
50
+ option :pipeline, type: :string
51
+ define_method 'log_drain:create:elasticsearch' do |handle|
52
+ account = ensure_environment(options)
53
+ database = ensure_database(options)
54
+
55
+ opts = {
56
+ handle: handle,
57
+ database_id: database.id,
58
+ logging_token: options[:pipeline],
59
+ drain_apps: options[:drain_apps],
60
+ drain_databases: options[:drain_databases],
61
+ drain_ephemeral_sessions: options[:drain_ephemeral_sessions],
62
+ drain_proxies: options[:drain_proxies],
63
+ drain_type: :elasticsearch_database
64
+ }
65
+
66
+ create_log_drain(account, opts)
67
+ end
68
+
69
+ desc 'log_drain:create:datadog HANDLE ' \
70
+ '--url DATADOG_URL ' \
71
+ + drain_flags,
72
+ 'Create a Datadog Log Drain'
73
+ drain_options
74
+ option :url, type: :string
75
+ define_method 'log_drain:create:datadog' do |handle|
76
+ msg = 'Must be in the format of ' \
77
+ '"https://http-intake.logs.datadoghq.com' \
78
+ '/v1/input/<DD_API_KEY>".'
79
+ create_https_based_log_drain(handle, options, url_format_msg: msg)
80
+ end
81
+
82
+ desc 'log_drain:create:https HANDLE ' \
83
+ '--url URL ' \
84
+ + drain_flags,
85
+ 'Create a HTTPS Drain'
86
+ option :url, type: :string
87
+ drain_options
88
+ define_method 'log_drain:create:https' do |handle|
89
+ create_https_based_log_drain(handle, options)
90
+ end
91
+
92
+ desc 'log_drain:create:sumologic HANDLE ' \
93
+ '--url SUMOLOGIC_URL ' \
94
+ + drain_flags,
95
+ 'Create a Sumologic Drain'
96
+ option :url, type: :string
97
+ drain_options
98
+ define_method 'log_drain:create:sumologic' do |handle|
99
+ create_https_based_log_drain(handle, options)
100
+ end
101
+
102
+ desc 'log_drain:create:logdna HANDLE ' \
103
+ '--url LOGDNA_URL ' \
104
+ + drain_flags,
105
+ 'Create a LogDNA Log Drain'
106
+ option :url, type: :string
107
+ drain_options
108
+ define_method 'log_drain:create:logdna' do |handle|
109
+ msg = 'Must be in the format of ' \
110
+ '"https://logs.logdna.com/aptible/ingest/<INGESTION KEY>".'
111
+ create_https_based_log_drain(handle, options, url_format_msg: msg)
112
+ end
113
+
114
+ desc 'log_drain:create:papertrail HANDLE ' \
115
+ '--host PAPERTRAIL_HOST --port PAPERTRAIL_PORT ' \
116
+ + drain_flags,
117
+ 'Create a Papertrail Log Drain'
118
+ option :host, type: :string
119
+ option :port, type: :string
120
+ drain_options
121
+ define_method 'log_drain:create:papertrail' do |handle|
122
+ create_syslog_based_log_drain(handle, options)
123
+ end
124
+
125
+ desc 'log_drain:create:syslog HANDLE ' \
126
+ '--host SYSLOG_HOST --port SYSLOG_PORT ' \
127
+ '[--token TOKEN] ' \
128
+ + drain_flags,
129
+ 'Create a Papertrail Log Drain'
130
+ option :host, type: :string
131
+ option :port, type: :string
132
+ option :token, type: :string
133
+ drain_options
134
+ define_method 'log_drain:create:syslog' do |handle|
135
+ create_syslog_based_log_drain(handle, options)
136
+ end
137
+
138
+ desc 'log_drain:deprovision HANDLE --environment ENVIRONMENT',
139
+ 'Deprovisions a log drain'
140
+ option :environment
141
+ define_method 'log_drain:deprovision' do |handle|
142
+ account = ensure_environment(options)
143
+ drain = ensure_log_drain(account, handle)
144
+ op = drain.create_operation(type: :deprovision)
145
+ begin
146
+ attach_to_operation_logs(op)
147
+ rescue HyperResource::ClientError => e
148
+ # A 404 here means that the operation completed successfully,
149
+ # and was removed faster than attach_to_operation_logs
150
+ # could attach to the logs.
151
+ raise if e.response.status != 404
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,141 @@
1
+ module Aptible
2
+ module CLI
3
+ module Subcommands
4
+ module MetricDrain
5
+ SITES = {
6
+ 'US1' => 'https://app.datadoghq.com',
7
+ 'US3' => 'https://us3.datadoghq.com',
8
+ 'EU1' => 'https://app.datadoghq.eu',
9
+ 'US1-FED' => 'https://app.ddog-gov.com'
10
+ }.freeze
11
+
12
+ def self.included(thor)
13
+ thor.class_eval do
14
+ include Helpers::Token
15
+ include Helpers::Database
16
+ include Helpers::MetricDrain
17
+
18
+ desc 'metric_drain:list', 'List all Metric Drains'
19
+ option :environment
20
+ define_method 'metric_drain:list' do
21
+ Formatter.render(Renderer.current) do |root|
22
+ root.grouped_keyed_list(
23
+ { 'environment' => 'handle' },
24
+ 'handle'
25
+ ) do |node|
26
+ scoped_environments(options).each do |account|
27
+ account.metric_drains.each do |drain|
28
+ node.object do |n|
29
+ ResourceFormatter.inject_metric_drain(n, drain, account)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ desc 'metric_drain:create:influxdb HANDLE '\
38
+ '--db DATABASE_HANDLE --environment ENVIRONMENT',
39
+ 'Create an InfluxDB Metric Drain'
40
+ option :db, type: :string
41
+ option :environment
42
+
43
+ define_method 'metric_drain:create:influxdb' do |handle|
44
+ account = ensure_environment(options)
45
+ database = ensure_database(options)
46
+
47
+ opts = {
48
+ handle: handle,
49
+ database_id: database.id,
50
+ drain_type: :influxdb_database
51
+ }
52
+
53
+ create_metric_drain(account, opts)
54
+ end
55
+
56
+ desc 'metric_drain:create:influxdb:custom HANDLE '\
57
+ '--username USERNAME --password PASSWORD ' \
58
+ '--url URL_INCLUDING_PORT ' \
59
+ '--db INFLUX_DATABASE_NAME ' \
60
+ '--environment ENVIRONMENT',
61
+ 'Create an InfluxDB Metric Drain'
62
+ option :db, type: :string
63
+ option :username, type: :string
64
+ option :password, type: :string
65
+ option :url, type: :string
66
+ option :db, type: :string
67
+ option :environment
68
+ define_method 'metric_drain:create:influxdb:custom' do |handle|
69
+ account = ensure_environment(options)
70
+
71
+ config = {
72
+ address: options[:url],
73
+ username: options[:username],
74
+ password: options[:password],
75
+ database: options[:db]
76
+ }
77
+ opts = {
78
+ handle: handle,
79
+ drain_configuration: config,
80
+ drain_type: :influxdb
81
+ }
82
+
83
+ create_metric_drain(account, opts)
84
+ end
85
+
86
+ desc 'metric_drain:create:datadog HANDLE '\
87
+ '--api_key DATADOG_API_KEY '\
88
+ '--site DATADOG_SITE ' \
89
+ '--environment ENVIRONMENT',
90
+ 'Create a Datadog Metric Drain'
91
+ option :api_key, type: :string
92
+ option :site, type: :string
93
+ option :environment
94
+ define_method 'metric_drain:create:datadog' do |handle|
95
+ account = ensure_environment(options)
96
+
97
+ config = {
98
+ api_key: options[:api_key]
99
+ }
100
+ unless options[:site].nil?
101
+ site = SITES[options[:site]]
102
+
103
+ unless site
104
+ sites = SITES.keys.join(', ')
105
+ raise Thor::Error, 'Invalid Datadog site. ' \
106
+ "Valid options are #{sites}"
107
+ end
108
+
109
+ config[:series_url] = site
110
+ end
111
+ opts = {
112
+ handle: handle,
113
+ drain_type: :datadog,
114
+ drain_configuration: config
115
+ }
116
+
117
+ create_metric_drain(account, opts)
118
+ end
119
+
120
+ desc 'metric_drain:deprovision HANDLE --environment ENVIRONMENT',
121
+ 'Deprovisions a Metric Drain'
122
+ option :environment
123
+ define_method 'metric_drain:deprovision' do |handle|
124
+ account = ensure_environment(options)
125
+ drain = ensure_metric_drain(account, handle)
126
+ op = drain.create_operation(type: :deprovision)
127
+ begin
128
+ attach_to_operation_logs(op)
129
+ rescue HyperResource::ClientError => e
130
+ # A 404 here means that the operation completed successfully,
131
+ # and was removed faster than attach_to_operation_logs
132
+ # could attach to the logs.
133
+ raise if e.response.status != 404
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.18.2'.freeze
3
+ VERSION = '0.19.2'.freeze
4
4
  end
5
5
  end
@@ -253,42 +253,6 @@ describe Aptible::CLI::Agent do
253
253
  .to raise_error(/provide at least/im)
254
254
  end
255
255
 
256
- it 'should scale container count (legacy)' do
257
- stub_options
258
- expect(service).to receive(:create_operation!)
259
- .with(type: 'scale', container_count: 3)
260
- .and_return(op)
261
- subject.send('apps:scale', 'web', '3')
262
- expect(captured_logs).to match(/deprecated/i)
263
- end
264
-
265
- it 'should scale container size (legacy)' do
266
- stub_options(size: 90210)
267
- expect(service).to receive(:create_operation!)
268
- .with(type: 'scale', container_size: 90210)
269
- .and_return(op)
270
- subject.send('apps:scale', 'web')
271
- expect(captured_logs).to match(/deprecated/i)
272
- end
273
-
274
- it 'should fail when using both current and legacy count' do
275
- stub_options(container_count: 2)
276
- expect { subject.send('apps:scale', 'web', '3') }
277
- .to raise_error(/count was passed via both/im)
278
- end
279
-
280
- it 'should fail when using both current and legacy size' do
281
- stub_options(container_size: 1024, size: 512)
282
- expect { subject.send('apps:scale', 'web') }
283
- .to raise_error(/size was passed via both/im)
284
- end
285
-
286
- it 'should fail when using too many arguments' do
287
- stub_options
288
- expect { subject.send('apps:scale', 'web', '3', '4') }
289
- .to raise_error(/usage:.*apps:scale/im)
290
- end
291
-
292
256
  it 'should fail if the service does not exist' do
293
257
  stub_options(container_count: 2)
294
258
 
@@ -323,17 +287,6 @@ describe Aptible::CLI::Agent do
323
287
  subject.send('apps:scale', 'web')
324
288
  end.to raise_error(Thor::Error)
325
289
  end
326
-
327
- it 'should fail if number is not a valid number (legacy)' do
328
- allow(subject).to receive(:options) { { app: 'hello' } }
329
- allow(service).to receive(:create_operation) { op }
330
-
331
- expect do
332
- subject.send('apps:scale', 'web', 'potato')
333
- end.to raise_error(ArgumentError)
334
-
335
- expect(captured_logs).to match(/deprecated/i)
336
- end
337
290
  end
338
291
 
339
292
  describe '#apps:deprovision' do
@@ -112,7 +112,7 @@ describe Aptible::CLI::Agent do
112
112
  Fabricate(:database, account: account, handle: default_handle)
113
113
  end
114
114
 
115
- subject.options = { size: s }
115
+ subject.options = { disk_size: s }
116
116
  subject.send('backup:restore', 1)
117
117
  end
118
118
 
@@ -57,16 +57,6 @@ describe Aptible::CLI::Agent do
57
57
  subject.send('db:create', 'foo')
58
58
  end
59
59
 
60
- it 'creates a new DB with a (implicitly) disk size' do
61
- expect_provision_database(
62
- { handle: 'foo', type: 'postgresql', initial_disk_size: 200 },
63
- { disk_size: 200 }
64
- )
65
-
66
- subject.options = { type: 'postgresql', size: 200 }
67
- subject.send('db:create', 'foo')
68
- end
69
-
70
60
  it 'creates a new DB with a disk-size' do
71
61
  expect_provision_database(
72
62
  { handle: 'foo', type: 'postgresql', initial_disk_size: 200 },
@@ -414,18 +404,6 @@ describe Aptible::CLI::Agent do
414
404
  expect(captured_logs).to match(/restarting foobar/i)
415
405
  end
416
406
 
417
- it 'allows restarting a database with (implicitly disk) size' do
418
- expect(database).to receive(:create_operation!)
419
- .with(type: 'restart', disk_size: 40).and_return(op)
420
-
421
- expect(subject).to receive(:attach_to_operation_logs).with(op)
422
-
423
- subject.options = { size: 40 }
424
- subject.send('db:restart', handle)
425
-
426
- expect(captured_logs).to match(/restarting foobar/i)
427
- end
428
-
429
407
  it 'allows restarting a database with a disk-size' do
430
408
  expect(database).to receive(:create_operation!)
431
409
  .with(type: 'restart', disk_size: 40).and_return(op)
@@ -581,10 +559,6 @@ describe Aptible::CLI::Agent do
581
559
  expect_replicate_database(container_size: 40)
582
560
  end
583
561
 
584
- it 'allows replicating a database with an (implicitly) disk size option' do
585
- expect_replicate_database(size: 40)
586
- end
587
-
588
562
  it 'allows replicating a database with a disk-size option' do
589
563
  expect_replicate_database(disk_size: 40)
590
564
  end
@@ -94,6 +94,45 @@ describe Aptible::CLI::Agent do
94
94
  stub_options(ip_whitelist: %w(1.1.1.1))
95
95
  subject.send('endpoints:database:create', 'mydb')
96
96
  end
97
+
98
+ it 'creates an internal Database Endpoint' do
99
+ expect_create_vhost(db.service, internal: true)
100
+ stub_options(internal: true)
101
+ subject.send('endpoints:database:create', 'mydb')
102
+ end
103
+ end
104
+
105
+ describe 'endpoints:database:modify' do
106
+ it 'does not change anything if no options are passed' do
107
+ v = Fabricate(:vhost, service: db.service)
108
+ expect_modify_vhost(v, {})
109
+ stub_options(database: 'mydb')
110
+ subject.send('endpoints:database:modify', v.external_host)
111
+ end
112
+
113
+ it 'adds an IP whitelist' do
114
+ v = Fabricate(:vhost, service: db.service)
115
+ expect_modify_vhost(v, ip_whitelist: %w(1.1.1.1))
116
+
117
+ stub_options(database: 'mydb', ip_whitelist: %w(1.1.1.1))
118
+ subject.send('endpoints:database:modify', v.external_host)
119
+ end
120
+
121
+ it 'removes an IP whitelist' do
122
+ v = Fabricate(:vhost, service: db.service)
123
+ expect_modify_vhost(v, ip_whitelist: [])
124
+
125
+ stub_options(database: 'mydb', :'no-ip_whitelist' => true)
126
+ subject.send('endpoints:database:modify', v.external_host)
127
+ end
128
+
129
+ it 'does not allow disabling and adding an IP whitelist' do
130
+ v = Fabricate(:vhost, service: db.service)
131
+ stub_options(database: 'mydb', ip_whitelist: %w(1.1.1.1),
132
+ :'no-ip_whitelist' => true)
133
+ expect { subject.send('endpoints:database:modify', v.external_host) }
134
+ .to raise_error(/conflicting.*no-ip-whitelist.*ip-whitelist/im)
135
+ end
97
136
  end
98
137
 
99
138
  describe 'endpoints:list' do