tfwrapper 0.4.1 → 0.6.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -5
- data/.travis.yml +62 -0
- data/ChangeLog.md +31 -0
- data/Gemfile +4 -0
- data/README.md +48 -5
- data/lib/tfwrapper/helpers.rb +25 -3
- data/lib/tfwrapper/raketasks.rb +91 -10
- data/lib/tfwrapper/version.rb +1 -1
- data/spec/acceptance/acceptance_helpers.rb +16 -3
- data/spec/acceptance/acceptance_spec.rb +229 -0
- data/spec/fixtures/landscapeTest/Rakefile +38 -0
- data/spec/fixtures/landscapeTest/failingTerraform/main.tf +28 -0
- data/spec/fixtures/landscapeTest/main.tf +32 -0
- data/spec/fixtures/landscapeTest/state.json +43 -0
- data/spec/fixtures/landscapeTest/with_landscape_default.out +45 -0
- data/spec/fixtures/landscapeTest/with_landscape_dots.out +45 -0
- data/spec/fixtures/landscapeTest/with_landscape_lines.out +70 -0
- data/spec/fixtures/landscapeTest/with_landscape_stream.out +71 -0
- data/spec/fixtures/landscapeTest/without_landscape.out +62 -0
- data/spec/fixtures/testOne.tf +1 -0
- data/spec/fixtures/testThree/bar/testThreeBar.tf +1 -0
- data/spec/fixtures/testThree/baz/testThreeBaz.tf +1 -0
- data/spec/fixtures/testThree/foo/testThreeFoo.tf +1 -0
- data/spec/fixtures/testTwo/foo/bar/testTwo.tf +1 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/unit/helpers_spec.rb +131 -3
- data/spec/unit/raketasks_spec.rb +250 -26
- data/tfwrapper.gemspec +1 -0
- metadata +36 -4
- data/circle.yml +0 -28
data/lib/tfwrapper/version.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'ffi'
|
4
4
|
require 'faraday'
|
5
5
|
require 'json'
|
6
|
+
require 'retries'
|
6
7
|
|
7
8
|
def fixture_dir
|
8
9
|
File.absolute_path(
|
@@ -24,12 +25,24 @@ def desired_tf_version
|
|
24
25
|
return ENV['TF_VERSION']
|
25
26
|
end
|
26
27
|
# else get the latest release from GitHub
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
latest_tf_version
|
29
|
+
end
|
30
|
+
|
31
|
+
def latest_tf_version
|
32
|
+
resp = Faraday.get('https://checkpoint-api.hashicorp.com/v1/check/terraform')
|
33
|
+
rel = JSON.parse(resp.body)['current_version']
|
34
|
+
puts "Found latest terraform release as: #{rel}"
|
30
35
|
rel
|
31
36
|
end
|
32
37
|
|
38
|
+
# Given the example terraform plan output with placeholders for the
|
39
|
+
# latest terraform version and fixtures path, return the interpolated string.
|
40
|
+
def clean_tf_plan_output(raw_out, latest_ver, fixture_path)
|
41
|
+
raw_out
|
42
|
+
.gsub('%%TF_LATEST_VER%%', latest_ver)
|
43
|
+
.gsub('%%FIXTUREPATH%%', fixture_path)
|
44
|
+
end
|
45
|
+
|
33
46
|
class HashicorpFetcher
|
34
47
|
def initialize(program, version)
|
35
48
|
@program = program
|
@@ -18,6 +18,10 @@ else
|
|
18
18
|
APPLY_CMD = 'terraform apply'
|
19
19
|
end
|
20
20
|
|
21
|
+
without_landscape = !HAVE_LANDSCAPE && TF_VERSION == '0.11.2'
|
22
|
+
with_landscape = HAVE_LANDSCAPE && TF_VERSION == '0.11.2'
|
23
|
+
latest_tf_ver = latest_tf_version
|
24
|
+
|
21
25
|
Diplomat.configure do |config|
|
22
26
|
config.url = 'http://127.0.0.1:8500'
|
23
27
|
end
|
@@ -734,4 +738,229 @@ describe 'tfwrapper' do
|
|
734
738
|
end
|
735
739
|
end
|
736
740
|
end
|
741
|
+
context 'landscapeTest', order: :defined do
|
742
|
+
before(:all) do
|
743
|
+
@fixturepath = File.absolute_path(
|
744
|
+
File.join(File.dirname(__FILE__), '..', 'fixtures', 'landscapeTest')
|
745
|
+
)
|
746
|
+
end
|
747
|
+
before(:each) do
|
748
|
+
Diplomat::Kv.put(
|
749
|
+
'landscapeTest/foo', '{"bar":"barval","baz":"bazval","foo":"fooval"}'
|
750
|
+
)
|
751
|
+
Diplomat::Kv.put(
|
752
|
+
'terraform/landscapeTest',
|
753
|
+
File.read(File.join(@fixturepath, 'state.json'))
|
754
|
+
)
|
755
|
+
end
|
756
|
+
context 'without landscape installed', if: without_landscape do
|
757
|
+
describe 'default_tf:plan' do
|
758
|
+
before(:all) do
|
759
|
+
@out_err, @ecode = Open3.capture2e(
|
760
|
+
'timeout -k 60 45 bundle exec rake default_tf:plan',
|
761
|
+
chdir: @fixturepath
|
762
|
+
)
|
763
|
+
@varpath = File.join(@fixturepath, 'default_build.tfvars.json')
|
764
|
+
end
|
765
|
+
after(:all) do
|
766
|
+
File.delete(@varpath) if File.file?(@varpath)
|
767
|
+
end
|
768
|
+
it 'does not time out' do
|
769
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
770
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
771
|
+
end
|
772
|
+
it 'exits zero' do
|
773
|
+
expect(@ecode.exitstatus).to eq(0)
|
774
|
+
end
|
775
|
+
it 'returns unmodified terraform output' do
|
776
|
+
expected = clean_tf_plan_output(
|
777
|
+
File.read(File.join(@fixturepath, 'without_landscape.out')),
|
778
|
+
latest_tf_ver, @fixturepath
|
779
|
+
)
|
780
|
+
expect(@out_err.strip).to eq(expected.strip)
|
781
|
+
end
|
782
|
+
end
|
783
|
+
end
|
784
|
+
context 'with landscape installed', if: with_landscape do
|
785
|
+
context 'and disabled' do
|
786
|
+
describe 'disabled_tf:plan' do
|
787
|
+
before(:all) do
|
788
|
+
@out_err, @ecode = Open3.capture2e(
|
789
|
+
'timeout -k 60 45 bundle exec rake disabled_tf:plan',
|
790
|
+
chdir: @fixturepath
|
791
|
+
)
|
792
|
+
@varpath = File.join(@fixturepath, 'disabled_build.tfvars.json')
|
793
|
+
end
|
794
|
+
after(:all) do
|
795
|
+
File.delete(@varpath) if File.file?(@varpath)
|
796
|
+
end
|
797
|
+
it 'does not time out' do
|
798
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
799
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
800
|
+
end
|
801
|
+
it 'exits zero' do
|
802
|
+
expect(@ecode.exitstatus).to eq(0)
|
803
|
+
end
|
804
|
+
it 'returns unmodified terraform output' do
|
805
|
+
expected = clean_tf_plan_output(
|
806
|
+
File.read(File.join(@fixturepath, 'without_landscape.out')),
|
807
|
+
latest_tf_ver, @fixturepath
|
808
|
+
).gsub('default_build.tfvars.json', 'disabled_build.tfvars.json')
|
809
|
+
expect(@out_err.strip).to eq(expected.strip)
|
810
|
+
end
|
811
|
+
end
|
812
|
+
end
|
813
|
+
context 'and default progress' do
|
814
|
+
describe 'default_tf:plan' do
|
815
|
+
before(:all) do
|
816
|
+
@out_err, @ecode = Open3.capture2e(
|
817
|
+
'timeout -k 60 45 bundle exec rake default_tf:plan',
|
818
|
+
chdir: @fixturepath
|
819
|
+
)
|
820
|
+
@varpath = File.join(@fixturepath, 'default_build.tfvars.json')
|
821
|
+
end
|
822
|
+
after(:all) do
|
823
|
+
File.delete(@varpath) if File.file?(@varpath)
|
824
|
+
end
|
825
|
+
it 'does not time out' do
|
826
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
827
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
828
|
+
end
|
829
|
+
it 'exits zero' do
|
830
|
+
expect(@ecode.exitstatus).to eq(0)
|
831
|
+
end
|
832
|
+
it 'returns landscape output and no plan output' do
|
833
|
+
expected = clean_tf_plan_output(
|
834
|
+
File.read(File.join(@fixturepath, 'with_landscape_default.out')),
|
835
|
+
latest_tf_ver, @fixturepath
|
836
|
+
)
|
837
|
+
expect(@out_err.strip).to eq(expected.strip)
|
838
|
+
end
|
839
|
+
end
|
840
|
+
end
|
841
|
+
context 'and dots progress' do
|
842
|
+
describe 'dots_tf:plan' do
|
843
|
+
before(:all) do
|
844
|
+
@out_err, @ecode = Open3.capture2e(
|
845
|
+
'timeout -k 60 45 bundle exec rake dots_tf:plan',
|
846
|
+
chdir: @fixturepath
|
847
|
+
)
|
848
|
+
@varpath = File.join(@fixturepath, 'dots_build.tfvars.json')
|
849
|
+
end
|
850
|
+
after(:all) do
|
851
|
+
File.delete(@varpath) if File.file?(@varpath)
|
852
|
+
end
|
853
|
+
it 'does not time out' do
|
854
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
855
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
856
|
+
end
|
857
|
+
it 'exits zero' do
|
858
|
+
expect(@ecode.exitstatus).to eq(0)
|
859
|
+
end
|
860
|
+
it 'returns progress dots for plan output and landscape output' do
|
861
|
+
expected = clean_tf_plan_output(
|
862
|
+
File.read(File.join(@fixturepath, 'with_landscape_dots.out')),
|
863
|
+
latest_tf_ver, @fixturepath
|
864
|
+
)
|
865
|
+
expect(@out_err.strip).to eq(expected.strip)
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end
|
869
|
+
context 'and lines progress' do
|
870
|
+
describe 'lines_tf:plan' do
|
871
|
+
before(:all) do
|
872
|
+
@out_err, @ecode = Open3.capture2e(
|
873
|
+
'timeout -k 60 45 bundle exec rake lines_tf:plan',
|
874
|
+
chdir: @fixturepath
|
875
|
+
)
|
876
|
+
@varpath = File.join(@fixturepath, 'lines_build.tfvars.json')
|
877
|
+
end
|
878
|
+
after(:all) do
|
879
|
+
File.delete(@varpath) if File.file?(@varpath)
|
880
|
+
end
|
881
|
+
it 'does not time out' do
|
882
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
883
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
884
|
+
end
|
885
|
+
it 'exits zero' do
|
886
|
+
expect(@ecode.exitstatus).to eq(0)
|
887
|
+
end
|
888
|
+
it 'returns progress lines for plan output and landscape output' do
|
889
|
+
expected = clean_tf_plan_output(
|
890
|
+
File.read(File.join(@fixturepath, 'with_landscape_lines.out')),
|
891
|
+
latest_tf_ver, @fixturepath
|
892
|
+
)
|
893
|
+
expect(@out_err.strip).to eq(expected.strip)
|
894
|
+
end
|
895
|
+
end
|
896
|
+
end
|
897
|
+
context 'and stream progress' do
|
898
|
+
describe 'stream_tf:plan' do
|
899
|
+
before(:all) do
|
900
|
+
@out_err, @ecode = Open3.capture2e(
|
901
|
+
'timeout -k 60 45 bundle exec rake stream_tf:plan',
|
902
|
+
chdir: @fixturepath
|
903
|
+
)
|
904
|
+
@varpath = File.join(@fixturepath, 'stream_build.tfvars.json')
|
905
|
+
end
|
906
|
+
after(:all) do
|
907
|
+
File.delete(@varpath) if File.file?(@varpath)
|
908
|
+
end
|
909
|
+
it 'does not time out' do
|
910
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
911
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
912
|
+
end
|
913
|
+
it 'exits zero' do
|
914
|
+
expect(@ecode.exitstatus).to eq(0)
|
915
|
+
end
|
916
|
+
it 'returns streaming plan output and landscape output' do
|
917
|
+
expected = clean_tf_plan_output(
|
918
|
+
File.read(File.join(@fixturepath, 'with_landscape_stream.out')),
|
919
|
+
latest_tf_ver, @fixturepath
|
920
|
+
)
|
921
|
+
expect(@out_err.strip).to eq(expected.strip)
|
922
|
+
end
|
923
|
+
end
|
924
|
+
end
|
925
|
+
context 'when terraform fails' do
|
926
|
+
describe 'failing_tf:plan' do
|
927
|
+
before(:all) do
|
928
|
+
@out_err, @ecode = Open3.capture2e(
|
929
|
+
'timeout -k 60 45 bundle exec rake failing_tf:plan',
|
930
|
+
chdir: @fixturepath
|
931
|
+
)
|
932
|
+
@varpath = File.join(@fixturepath, 'failing_build.tfvars.json')
|
933
|
+
end
|
934
|
+
after(:all) do
|
935
|
+
File.delete(@varpath) if File.file?(@varpath)
|
936
|
+
end
|
937
|
+
it 'does not time out' do
|
938
|
+
expect(@ecode.exitstatus).to_not eq(124)
|
939
|
+
expect(@ecode.exitstatus).to_not eq(137)
|
940
|
+
end
|
941
|
+
it 'exits 1' do
|
942
|
+
expect(@ecode.exitstatus).to eq(1)
|
943
|
+
end
|
944
|
+
it 'returns the terraform output' do
|
945
|
+
expect(@out_err).to match(
|
946
|
+
/Terraform\sv0\.11\.2.*
|
947
|
+
terraform_runner\scommand:\s'terraform\sinit\s-input=false'.*
|
948
|
+
Running\swith:\sTerraform\sv0\.11\.2.*
|
949
|
+
Initializing\sthe\sbackend\.\.\..*
|
950
|
+
Successfully\sconfigured\sthe\sbackend\s"consul".*
|
951
|
+
Terraform\shas\sbeen\ssuccessfully\sinitialized.*
|
952
|
+
terraform_runner\scommand\s'terraform\sinit\s-input=false'\s
|
953
|
+
finished\sand\sexited\s0.*
|
954
|
+
consul_key_prefix\.landscapeTest:\s"path_prefix":\srequired\s
|
955
|
+
field\sis\snot\sset.*
|
956
|
+
rake\saborted.*
|
957
|
+
StandardError:\sErrors\shave\soccurred\sexecuting:\s
|
958
|
+
'terraform\splan\s-var-file.*
|
959
|
+
Tasks:\sTOP\s=>\sfailing_tf:plan.*/xm
|
960
|
+
)
|
961
|
+
end
|
962
|
+
end
|
963
|
+
end
|
964
|
+
end
|
965
|
+
end
|
737
966
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tfwrapper/raketasks'
|
4
|
+
|
5
|
+
TFWrapper::RakeTasks.install_tasks(
|
6
|
+
'.',
|
7
|
+
namespace_prefix: 'default'
|
8
|
+
)
|
9
|
+
|
10
|
+
TFWrapper::RakeTasks.install_tasks(
|
11
|
+
'.',
|
12
|
+
namespace_prefix: 'disabled',
|
13
|
+
disable_landscape: true
|
14
|
+
)
|
15
|
+
|
16
|
+
TFWrapper::RakeTasks.install_tasks(
|
17
|
+
'.',
|
18
|
+
namespace_prefix: 'stream',
|
19
|
+
landscape_progress: :stream
|
20
|
+
)
|
21
|
+
|
22
|
+
TFWrapper::RakeTasks.install_tasks(
|
23
|
+
'.',
|
24
|
+
namespace_prefix: 'dots',
|
25
|
+
landscape_progress: :dots
|
26
|
+
)
|
27
|
+
|
28
|
+
TFWrapper::RakeTasks.install_tasks(
|
29
|
+
'.',
|
30
|
+
namespace_prefix: 'lines',
|
31
|
+
landscape_progress: :lines
|
32
|
+
)
|
33
|
+
|
34
|
+
TFWrapper::RakeTasks.install_tasks(
|
35
|
+
'failingTerraform/',
|
36
|
+
namespace_prefix: 'failing',
|
37
|
+
landscape_progress: :dots
|
38
|
+
)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
terraform {
|
2
|
+
required_version = "> 0.9.0"
|
3
|
+
backend "consul" {
|
4
|
+
address = "127.0.0.1:8500"
|
5
|
+
path = "terraform/landscapeTestFailure"
|
6
|
+
}
|
7
|
+
}
|
8
|
+
|
9
|
+
provider "consul" {
|
10
|
+
address = "127.0.0.1:8500"
|
11
|
+
version = "1.0.0"
|
12
|
+
}
|
13
|
+
|
14
|
+
locals {
|
15
|
+
keys = {
|
16
|
+
foo = "foo2val"
|
17
|
+
bar = "bar2val"
|
18
|
+
baz = "baz2val"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
variable "foo" { default = "bar" }
|
23
|
+
|
24
|
+
resource "consul_key_prefix" "landscapeTest" {
|
25
|
+
invalid_param = "whoCares"
|
26
|
+
}
|
27
|
+
|
28
|
+
output "foo_variable" { value = "${var.foo}" }
|
@@ -0,0 +1,32 @@
|
|
1
|
+
terraform {
|
2
|
+
required_version = "> 0.9.0"
|
3
|
+
backend "consul" {
|
4
|
+
address = "127.0.0.1:8500"
|
5
|
+
path = "terraform/landscapeTest"
|
6
|
+
}
|
7
|
+
}
|
8
|
+
|
9
|
+
provider "consul" {
|
10
|
+
address = "127.0.0.1:8500"
|
11
|
+
version = "1.0.0"
|
12
|
+
}
|
13
|
+
|
14
|
+
locals {
|
15
|
+
keys = {
|
16
|
+
foo = "foo2val"
|
17
|
+
bar = "bar2val"
|
18
|
+
baz = "baz2val"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
variable "foo" { default = "bar" }
|
23
|
+
|
24
|
+
resource "consul_key_prefix" "landscapeTest" {
|
25
|
+
path_prefix = "landscapeTest/"
|
26
|
+
|
27
|
+
subkeys = {
|
28
|
+
foo = "${jsonencode(local.keys)}"
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
output "foo_variable" { value = "${var.foo}" }
|
@@ -0,0 +1,43 @@
|
|
1
|
+
{
|
2
|
+
"version": 3,
|
3
|
+
"terraform_version": "0.11.2",
|
4
|
+
"serial": 1,
|
5
|
+
"lineage": "2bab32f5-67fc-4210-8a74-af61d21a5420",
|
6
|
+
"modules": [
|
7
|
+
{
|
8
|
+
"path": [
|
9
|
+
"root"
|
10
|
+
],
|
11
|
+
"outputs": {
|
12
|
+
"foo_variable": {
|
13
|
+
"sensitive": false,
|
14
|
+
"type": "string",
|
15
|
+
"value": "bar"
|
16
|
+
}
|
17
|
+
},
|
18
|
+
"resources": {
|
19
|
+
"consul_key_prefix.landscapeTest": {
|
20
|
+
"type": "consul_key_prefix",
|
21
|
+
"depends_on": [
|
22
|
+
"local.keys"
|
23
|
+
],
|
24
|
+
"primary": {
|
25
|
+
"id": "landscapeTest/",
|
26
|
+
"attributes": {
|
27
|
+
"datacenter": "dc1",
|
28
|
+
"id": "landscapeTest/",
|
29
|
+
"path_prefix": "landscapeTest/",
|
30
|
+
"subkeys.%": "1",
|
31
|
+
"subkeys.foo": "{\"bar\":\"bar2val\",\"baz\":\"baz2val\",\"foo\":\"foo2val\"}"
|
32
|
+
},
|
33
|
+
"meta": {},
|
34
|
+
"tainted": false
|
35
|
+
},
|
36
|
+
"deposed": [],
|
37
|
+
"provider": "provider.consul"
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"depends_on": []
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Terraform v0.11.2
|
2
|
+
|
3
|
+
Your version of Terraform is out of date! The latest version
|
4
|
+
is %%TF_LATEST_VER%%. You can update by downloading from www.terraform.io/downloads.html
|
5
|
+
terraform_runner command: 'terraform init -input=false' (in %%FIXTUREPATH%%)
|
6
|
+
Running with: Terraform v0.11.2
|
7
|
+
|
8
|
+
Your version of Terraform is out of date! The latest version
|
9
|
+
is %%TF_LATEST_VER%%. You can update by downloading from www.terraform.io/downloads.html
|
10
|
+
|
11
|
+
[0m[1mInitializing the backend...[0m
|
12
|
+
[0m[32m
|
13
|
+
Successfully configured the backend "consul"! Terraform will automatically
|
14
|
+
use this backend unless the backend configuration changes.[0m
|
15
|
+
|
16
|
+
[0m[1mInitializing provider plugins...[0m
|
17
|
+
- Checking for available provider plugins on https://releases.hashicorp.com...
|
18
|
+
- Downloading plugin for provider "consul" (1.0.0)...
|
19
|
+
|
20
|
+
[0m[1m[32mTerraform has been successfully initialized![0m[32m[0m
|
21
|
+
[0m[32m
|
22
|
+
You may now begin working with Terraform. Try running "terraform plan" to see
|
23
|
+
any changes that are required for your infrastructure. All Terraform commands
|
24
|
+
should now work.
|
25
|
+
|
26
|
+
If you ever set or change modules or backend configuration for Terraform,
|
27
|
+
rerun this command to reinitialize your working directory. If you forget, other
|
28
|
+
commands will detect it and remind you to do so if necessary.[0m
|
29
|
+
terraform_runner command 'terraform init -input=false' finished and exited 0
|
30
|
+
Terraform vars written to: %%FIXTUREPATH%%/default_build.tfvars.json
|
31
|
+
terraform_runner command: 'terraform plan -var-file %%FIXTUREPATH%%/default_build.tfvars.json' (in %%FIXTUREPATH%%)
|
32
|
+
Terraform vars:
|
33
|
+
terraform_runner command 'terraform plan -var-file %%FIXTUREPATH%%/default_build.tfvars.json' finished and exited 0
|
34
|
+
[0;33;49m~ consul_key_prefix.landscapeTest[0m
|
35
|
+
[0;33;49m subkeys.foo: [0m{
|
36
|
+
[31m- "bar": "barval",[0m
|
37
|
+
[31m- "baz": "bazval",[0m
|
38
|
+
[31m- "foo": "fooval"[0m
|
39
|
+
[32m+ "bar": "bar2val",[0m
|
40
|
+
[32m+ "baz": "baz2val",[0m
|
41
|
+
[32m+ "foo": "foo2val"[0m
|
42
|
+
}
|
43
|
+
|
44
|
+
Plan: 0 to add, 1 to change, 0 to destroy.
|
45
|
+
|