From b05f0b0e7dd7e295e3c72a36c18cb96ea78102a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Christoph=20K=C3=BCster?= Date: Mon, 11 May 2020 22:28:03 +0200 Subject: [PATCH] Filter resources NOT having a tag (#101) * updated to allow for all resources not matching tag to be removed * better example of use * Refactor NewFilter function * move unmarshal JSON into NewFilter * return error instead of Fatal() * Add tagged filter If true, select all resources with any tag. If false, select all resources without tags. * Check len(tags) == 0 instead of tags != nil * Separate MatchTags and MatchTagged * Add notags filter * Remove notags and allow NOT(key) in tags filter * Update README * Fix using matchTags and matchNoTags together Co-authored-by: Richard Silver --- Makefile | 2 +- README.md | 143 +++++++++--------- command/wipe.go | 11 +- command/wrapped_main.go | 2 +- img/logo.png | Bin 0 -> 26160 bytes resource/filter.go | 184 ++++++++++++++++------- resource/filter_test.go | 316 +++++++++++++++++++++++++++++++++++++--- resource/select.go | 12 +- resource/select_test.go | 115 ++++++--------- 9 files changed, 554 insertions(+), 231 deletions(-) create mode 100644 img/logo.png diff --git a/Makefile b/Makefile index 00c81e6af..47e7d10e5 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ test: ## Run unit tests .PHONY: test-all test-all: ## Run tests (including acceptance and integration tests) go clean -testcache ${PKG_LIST} - ./bin/go-acc ${PKG_LIST} -- -v -$(TESTARGS) -p 1 -race -timeout 30m + ./bin/go-acc ${PKG_LIST} -- -v $(TESTARGS) -p 1 -race -timeout 30m .PHONY: build build: ## Build binary diff --git a/README.md b/README.md index dbec9ba51..7010c52fa 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,21 @@ -# AWSweeper - -

- - Release - - - pipeline status - - - Go Report - - - Go Doc - - - Software License - +

+ AWSweeper Logo +

AWSweeper

+

A tool for cleaning your AWS account

-AWSweeper wipes out all (or parts) of the resources in your AWS account. Resources to be deleted can be filtered by their ID, tags or -creation date using [regular expressions](https://golang.org/pkg/regexp/syntax/) declared in a yaml file (see [config.yml](example/config.yml)). +--- +[![Release](https://img.shields.io/github/release/cloudetc/awsweeper.svg?style=for-the-badge)](https://github.com/cloudetc/awsweeper/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](/LICENSE.md) +[![Travis](https://img.shields.io/travis/cloudetc/awsweeper/master.svg?style=for-the-badge)](https://travis-ci.org/cloudetc/awsweeper) + +AWSweeper cleans out all (or parts) of the resources in your AWS account. Resources to be deleted can be filtered by +their ID, tags or creation date using [regular expressions](https://golang.org/pkg/regexp/syntax/) declared via a filter +in a YAML file (see [filter.yml](example/config.yml) as an example). AWSweeper [can delete many](#supported-resources), but not all resources yet. Your help -supporting more resources is very much appreciated ([please read this issue](https://github.com/cloudetc/awsweeper/issues/21) - to see how easy it is). Note that AWSweeper is based on the cloud-agnostic Terraform API for deletion - so it's planned to support - deleting Azure and Google Cloud Platform resources soon, too. +to support more resources is very much appreciated ([please read this issue](https://github.com/cloudetc/awsweeper/issues/21) + to see how easy it is). Happy erasing! @@ -32,99 +23,101 @@ Happy erasing! ## Installation -It's recommended to install a specific version of awsweeper available on the +It's recommended to install a specific version of AWSweeper available on the [releases page](https://github.com/cloudetc/awsweeper/releases). -Here is the recommended way to install awsweeper v0.6.0: +Here is the recommended way to install AWSweeper v0.8.0: ```bash # install it into ./bin/ -curl -sSfL https://raw.githubusercontent.com/cloudetc/awsweeper/master/install.sh | sh -s v0.6.0 +curl -sSfL https://raw.githubusercontent.com/cloudetc/awsweeper/master/install.sh | sh -s v0.8.0 ``` ## Usage - awsweeper [options] + awsweeper [options] To see options available run `awsweeper --help`. -## Filtering +## Filter -Resources to be deleted are filtered by a yaml configuration. To learn how, have a look at the following example: +Resources are deleted via a filter declared in a YAML file. aws_instance: + # instance filter part 1 - id: ^foo.* - tags: - foo: bar - bla: blub created: - before: 2018-06-14 - after: 2018-10-28 12:28:39.0000 + before: 2018-10-14 + after: 2018-06-28 12:28:39 + + # instance filter part 2 - tags: foo: bar - created: - before: 2018-06-14 - - tags: - foo: NOT(bar) - created: - after: 2018-06-14 - aws_iam_role: - -This config would delete all instances which ID matches `^foo.*` *AND* which have tags `foo: bar` *AND* `bla: blub` -*AND* which have been created between `2018-10-28 12:28:39 +0000 UTC` and `2018-06-14`. Additionally, it would delete instances -with tag `foo: bar` and which are older than `2018-06-14`. + NOT(owner): .* + + aws_security_groups: -Furthermore, this config would delete all IAM roles, as there is no list of filters provided for this resource type. +The filter snippet above deletes all EC2 instances that ID matches `^foo.*` and that have been created between + `2018-06-28 12:28:39` and `2018-10-14` UTC (instance filter part 1); additionally, EC2 instances having a tag + `foo: bar` *AND* not a tag key `owner` with any value are deleted (instance filter part 2); last but not least, + ALL security groups are deleted by this filter. -The general syntax of the filter config is as follows: +The general filter syntax is as follows: : - # filter 1 - id: | NOT() + tagged: bool (optional) tags: - : | NOT() + | NOT(key): | NOT() ... created: before: (optional) after: (optional) - # filter 2 + # OR - ... : ... -A more detailed description of the ways to filter resources: +Here is a more detailed description of the various ways to filter resources: -##### 1) All resources of a particular type +##### 1) Delete all resources of a particular type - [Terraform types](https://www.terraform.io/docs/providers/aws/index.html) are used to identify resources of a particular type - (e.g., `aws_security_group` selects all resources that are security groups, `aws_iam_role` all roles, - or `aws_instance` all EC2 instances). - - In the example above, by simply adding `security_group:` (no further filters for IDs or tags), - all security groups in your account would be deleted. Use the [all.yml](./all.yml), to delete all (currently supported) + [Terraform resource type indentifiers](https://www.terraform.io/docs/providers/aws/index.html) are used to delete + resources by type. The following filter snippet deletes *ALL* security groups, IAM roles, and EC2 instances: + + aws_security_group: + aws_iam_role: + aws_instance: + + Don't forget the `:` at the end of each line. Use the [all.yml](./all.yml), to delete all (currently supported) resources. -##### 2) By tags - - You can narrow down on particular types of resources by the tags they have. +##### 2) Delete by tags - If most of your resources have tags, this is probably the best to filter them - for deletion. But be aware: not all resources support tags and can be filtered this way. + If most of your resources have tags, this is probably the best way to filter them + for deletion. **Be aware**: Not all resources [support tags](#supported-resources) yet and can be filtered this way. + + The key and the value part of the tag filter can be negated by a surrounding `NOT(...)`. This allows for removing of + all resources not matching some tag key or value. In the example below, all EC2 instances without the `owner: me` + tag are deleted: - In the example above, all EC2 instances are terminated that have a tag with key `foo` and value `bar` as well as - `bla` and value `blub`. - - The tag filter can be negated by surrounding the regex with `NOT(...)` + aws_instance: + - tags: + NOT(Owner): me + + The flag `tagged: false` deletes all resources that have no tags. Contrary, resources with any tags can be deleted + with `tagged: true`: -##### 3) By ID + aws_instance: + - tagged: true - You can narrow down on particular types of resources by filtering on their IDs. +##### 3) Delete By ID - To see what the IDs of your resources are (could be their name, ARN, a random number), - run awsweeper in dry-run mode: `awsweeper --dry-run all.yml`. This way, nothing is deleted but - all the IDs and tags of your resources are printed. Then, use this information to create the yaml file. + You can narrow down on particular types of resources by filtering on based their IDs. - In the example above, all roles which name starts with `foo` are deleted (the ID of roles is their name). + To see what the ID of a resource is (could be its name, ARN, a random number), + run AWSweeper in dry-run mode: `awsweeper --dry-run all.yml`. This way, nothing is deleted but + all the IDs and tags of your resources are printed. Then, use this information to create the YAML config file. The id filter can be negated by surrounding the regex with `NOT(...)` @@ -152,8 +145,8 @@ A more detailed description of the ways to filter resources: ## Dry-run mode - Use `awsweeper --dry-run ` to only show what -would be deleted. This way, you can fine-tune your yaml configuration until it works the way you want it to. + Use `awsweeper --dry-run ` to only show what +would be deleted. This way, you can fine-tune your YAML filter configuration until it works the way you want it to. ## Supported resources diff --git a/command/wipe.go b/command/wipe.go index 32901dc0d..a300a38e9 100644 --- a/command/wipe.go +++ b/command/wipe.go @@ -62,11 +62,16 @@ func list(c *Wipe) []terradozerRes.DestroyableResource { // Run executes the wipe command. func (c *Wipe) Run(args []string) int { if len(args) == 1 { - c.filter = resource.NewFilter(args[0]) + filter, err := resource.NewFilter(args[0]) + if err != nil { + log.WithError(err).Fatal("failed to create resource filter") + } + + c.filter = filter - err := c.filter.Validate() + err = c.filter.Validate() if err != nil { - log.WithError(err).Fatal("failed to validate filter config") + log.WithError(err).Fatal("invalid filter config") } } else { fmt.Println(help()) diff --git a/command/wrapped_main.go b/command/wrapped_main.go index f2fdf749c..3dc8d53ef 100644 --- a/command/wrapped_main.go +++ b/command/wrapped_main.go @@ -21,7 +21,7 @@ import ( // WrappedMain is the actual main function that does not exit for acceptance testing purposes func WrappedMain() int { app := "awsweeper" - version := "v0.7.0" + version := "v0.8.0" set := flag.NewFlagSet(app, 0) versionFlag := set.Bool("version", false, "Show version") diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9022151295a4d73b2e814d5a072015d99458ad6e GIT binary patch literal 26160 zcmbq)^;eWp+chCWhcp9%NcV^gAq~=9Lzf8B-QC?tOE*Jzmm($I-QC^rj?eQwf5G>| zEEdDP&N}xM``UY-Lztqx1ST2~4Gsa8h4{RbBOtGmx9`x3B+nqHDl+`|JJ=Mk1X<4-E|spEHvAF!LK| zfvbH)lQd_fZmDz`v#jwo7bWyV_6J6^5A@+5LPI6{?mIheT%5mu|9-lRp6t>1^OA4% z*UECLO$Se{2luY$sauQ#Z6W-muUQv|0T6_l0+2=g|M^ud$1kn4Nv2f_wqiCXW?3nA zt63TJJmL>`lrT^*W#K=o3ze8=w-a8Z{6UC%&$)AODg#bpN$g$t4Ev+s!L*RV^25~| zQ35U7P!Jv0Aah$9V9F_wrGrC+`%KJr{z>#$fsIcpK^kl-_-J<905dm^rF z?1w(jYN#Rz-`Zi8ea_J(zXliebM^V=kY`p#cmsi?6iK?CZK{*aJnL+d;*zi-X(e^a zb!fO#xwf#aNq6zz<~T6x#uLe-Nul7Ylg#u%$>GTqHd)b!)nkaP&Ma$G!vX>kd%*OR zLRWb*~5ToMB3)jWH8 z<=|H3x4NAn)ApP87o??FCY!y{%5<%_CRJ$ggxI#bsiLM!~qr`J08|cLjCS(K*$qBhGuTnpK z`QaMDb)Y+TBF73_`swW)f-E9th2J*EW*$&R?7N<(hbO-u=*xv#PIVg9)3n8;GQrr# zdm*2!*)=Ae->Aa|EV&y;9Fsz*7HhzAIS*e=vqBsk6wy!J1bl&%R&^qq*M%35PZv2D z{;@4oU;}G<)Qe4h^yIuYB7}v-@ox zKwA@0_8R~S|HD)z1t9$eaq96prn2B!dgOSDl7cKe`8vU~Sa^WLi_JW9QD&=yeFzkw zX0Gsi#t5jvfvHI08uRC1d@}hh!`~O_PUPG@^dW1JwrPZ8w2lfzTWjwP^Y9&R2o+^P zy=jC+aV;!$KWMGUCxs@>*NAmreD82?R+gFxzSV84DMrpnj^D~nbfWrKr!CbrCU`o1 z(kQr7wUjILdoVK)edjIn^h4g_b7S*UIjEXkuE9s0c=*L-_)_GVb=6fOz1y5mcQH{G zQAPw27Plvf>;3J9O*Li%hT zau>H%4rds2Uy5VX+i8Pt1Lmjbf$MmOf;crm!(>t=eKS7SBa7%{ZXoe4PMy7W@?j%R z>DnN^I_*8muf74=eb3=*?+kUz4Mf=cog-Ear2`0+5X+s&a}2PC@6DNtT@@Ysq}Il= zkPKY|1bFT*(pDZ8KUCx?DSQf;E-aQl&H$|XDSH>{3jF|6 zzV3^zA_4_dwKW?T^O^qVBV4t=4Z!b7dd><>np)szXNpx|u~Bte?3G1~$oCs^N=lil zRZgJYvS0;?{EUq0L`LfYF;Nr?v5_r!(YUp#3K10)c+Agl^C+ ze9u&dQ4B~bKSHGwS7=e-|a6uN_5?O|^y z62Tc_mc7ma_lV%ebk6#%Akh~yGd2cmxSCV9wzmf)w4#Op|7bgGEA@+|eE&NPxvxgM zAO^rtbGSdF)^>?wdgLHTn;cPVV1RL%$&M(oZzL{+mMfD@B_q9?aT59aP=zF>cm2tPa26gUyi+DVBAygF$Yg}Oky@W z*t|Ub^+WWx-P8TAhQ!Q&)%^L&Z%>%i#ikmpVb9;G9%l|JAx@!M{02OAB~rcn?ONadK2HfnG^O(avVV0OA7;2_ z>{IV)-TuM^4p8&Sm?tUp3!V^~(<9GIj5D6jg4#&-6Cs9-C`GLuE)dARH+U7o8@n&e zh3!I2P_MNaced+XOMmqac&p@bpJXP|!HxF|hXhJ;uY-PshhQkl>c(nEGxgogr0O7# zg?ENy%oey;Tw)!r(AcXr*e3dKZ3Lq`9h~5g3na2QRoWEbr3q;lS{N=qT3Q+>LgCZc zCslChMa37G>0XRz7B*ohI<0V#1E3Kl)|-0Ms9sTOfBF{#QqO1BI;uPYx181(!Di}{ zGp~&jiGv*SwF>}iPcDjLkeCA*R4EQ8I3L*r0R7SNryCHEkUv!@X zzIaTBk(8BsY&rju%l8v?T=qVo#YWTXZVbL_}a}YTZPeQ?wl~r`(G06TN>#r<@m*4+g2KfM+|;}#0zK_)_eTAFI^r-4UFGl;y;bVJ0-)v;?aQuIW^0bRMc<#a z9!suFGzoTGjR-90&x7A%I9yoR;JWR9Fht+D1l!X6zxZ@wG)RxE+_e? z-hiVUdnOlW0ANZx5>GxGzCDfvf|3CuE0WU!WzU{63Gi% zu&TcIL?O2DeFzw|UXhkB5G>^J6-}Ww?ZQa%Vu8(uN<2N0*aP z)P!52#2r&_=qyw*DfPY7Yyi*>VC|+HVl&W}=PJHQ*;&zdPVWUGpI+fO;T0Hvq;gTK zLd;-qFle5NKjBNl{Ais5Ak_T_Yt}bEZ!VyNipR78iGhu2SG5jQuy7oqVCh^HsP?6r zRF!V>Eb6k7U@m2w?RzK0?L3GbS1MM~%Jmb;HH<>&xrJU-MB2Fc24u9^Wmr!&hYb>M zFKef{G5|M|1wKTbPvoS$jWwmnFtBfD=m<)^#{oRCj2#V>9m~1+(6|}H5_zv-sYFxZ zH(l59xM88A@cgD#s9!>%+g2U)>|W;lrVoY>#du^Y8@Jl*L!ScF*kgJKm1%)+!_n2| zM7pQ;2OM(}aO3(vWMLEkiJs#z7S@`RQo)t7S7X#e90AkR=8X|a)d26x`cqQ<*Ojl> zRweo>OByWMae)_*%L_d9RZ}eP3XC4b7`6bCGI#nRta5iu6u^OG-EKd`g150+I&;&N zsVnysh7mkGyht8 zVc}iU1hFw+k}@&Hva_ z^-9Y5U&x;>fCtCl1`Qu!nS2{=7&&gUuBisHiFAA%pbfJgrcg_M0Nz-`z^zQcs$p=l ztGtCKwXlqt#v0nOK!D*3%5R?kMK&W#QYu`fxjiJ~et0veqOiFVM^g4@x>2{hjl(u2 zNk-T{`lzJ6hqFg_+gEflXc!S6ZDbgkQFi=IpE+@$;m52_lCoaa>}bJhQqfP;PE~g+p{CKUdY z1}0P?y}S(!cUXkbSq38=FqPw`XovD^)E|T)8w_eIB)Krh8qx+ARJ+55;5nHkfn24Z zo)*HAD$j)O@%CcLqnvVjg`Yp@?*KC6p00$U1B^BA+te(n8vvk7S=r^ClpOC5_-6$L zlsrCyEQ`oT4EuIWYo4jxAb7AIr8Cd01b$WGl;{BbHJESAT>uLW4C?}6!)lP-oT)ks z<=&s;{S@6I7^Jt#6eYj`qn^Gdck1;_j#4YI4AGS6rni!Dj44F$(RYlkq<@~3?u>R# zuW-A<#}{%m^rq*Qci0O0v7PN}uLYZ(_U>8@Gr==mfV8uuyZnqT3#%4;`}B%+G^C6lKWqoA&n$Yv6(tpVkV|A)(1YdEw~*|2a&Wq)h^ zGGJzaT)xM4u6CSnJ!~P`pFCD*5qkltz3@W|^sVCAg0Z}2?TSdG)b7&AT~imOHUJ8| z85sbplOka~uebK$rGG;dUU@x(LBIJ;ZTk4#737BM>drLNl z&t*8P`nH9kn?n%^$Srm|$LpQ-h%(&49O(JB63qy#ZR-3U1ND5xcGymeU=RAuW*h@D zAOO_XKr|B~@H=U_)6s^uKLI`n$saIqf5}<<>xqK+#WQ<8K_-*bw3<`y7zhe5UX7KW zfl)}kyX&&D{F@8@_gjcPI2M!FsVMh} z)+}fJu*J>o=*P$M9^=2j?2pF*17?KLSqum&-y3a_01Mw69c;6$x@3Ks)05&QwK>TT zX`Sx+Qa1wwNSsw6@>XXA3xN^&uyTpW=j1K<4HlAd1?HH1Gb7kID$*5(VQnfgEqce9 zwY?3g;(2A#T{~p@0!FT@4H4hLY8*&Sa#7sA-PYYMmBFSU-i|4cpLpLvPAq0$J*o zOSOfMOVADsU=kC3cwPb%#B#a{KWRt8gNzkZnrVSgYZK|-ndI-ojR{1<}x}y(w zw8?79?~>jt2%^96(8F3cZKo;cXeKX?BDXE_P)9^j+6guMu~s^g+t(r zJf5f8z+mZb4;VWMQ>eE;-4|J2xEWszh^>s#Anv)j7|-Z7`)cS)2U8dW53f)iu-Zw+ z_8F4eiPb`enlHe4f*%7WNEDpE!$RW3a*d@b;^k+rhCcZ^l2GmLoaQij;eym>7CizS z&@4w&RHa$=! zW6iKA#r~w?hbjBun~RS|U>VBwt}O$bP}*$NQYn&ur9vXu#;DN(Iy^bK^RlLod`!Z* z=v{DxC-zb?z0rj6JXzu#1N#970{R+dYZ&$v@C^*MVt1rJgHD#|U+tCV>BDaxj&NkcSd*MLXV9@1(HpWBY zDQ4HlMpIOM0ojS*kC;Ew0GzY_T~Z}hUm%|?By6?Sun~06hN-XEf-O2;o?}q%aTkVs zN@^cXiMc#V_}9gBV%#($in)2M!pwx2k5;HQRwK)nCYqOdzlnCiMl65gitIq2cHF>2 zRIqHSQ~`{Q|7ZWrF-%TO+Tu1vB*)tXXq*X37ny3`zMql8f03YlRG2l5_DjqoY{la+ zkaT$@{}B(k8T5%)#k`mi z#3aMC-BHP1!PlwQ@KLQ+44wE#&x|_9EN-K;o-$1Itr%1Qk1x9JO%%jzbJiwg7 z0~TEdm@tLM={zvx;pTkJaAKOiec>rT3 zeyns@GZ|1Z3I8q+d0ekUAuX0|rO?73raGuaiASc%D5jl0KDOEa!+!frSrMmK3!+(z&UYXY77r}o9uTls)@_*E`YB%qw z{=iID40D+U6#bTkG$b*56o0NT?a+VHAh@KLc@dvmgP&rk7%O9@t2DPk3K7N_h%UO$ zaTTGXaR?;;htT)D4M?0#uw;Upq^)`ofZR%_cvw4zy3s|~++LYfjM*u(WB>%WPpN}eRBS75p7t2HVb1Yc*vN`FRjPJf z_32>6>>dz=4INk0b0W%b=fepmGn$6@T<^t>sVI`q@S-8%MG{yZSAkNhOIs=xZ-Qa4 zd;ZHNU%Qk0n828IcX0^4XACy-e{$}~9HT`4Kep^bYBubgiaQMxG`Lye1n zp^?MSjV%XAn`>j5Xh_KaETCC3F4a)^BThn5m={1O>gH@-4m8bEV6Ysyk1j8{x!?_l z1stnj%q>==o9A9Ah=Kz-X!xFl#RE>Q2v)ahxBaBY!8YTGpTAT#{AeP%L0ZkUW}}Ed zH*6C~`VRgGn*qETAFIb9Rz!?uuu^2)?^9`r2#7pwJfA+L!w#z*_K1O#!sQLOxHFlLO8^@yk5nmf+OtZD!#RX;U*T+hM{d%Z?sK#aGX=@Sh}g zx8u}z`v;6}f}P6RUUPbx!MG$j?VdJn>f<<|G7_?iw&^-gi(XzK;;WJ{v-kgBu5fxJ|#;bgR>i`%1h;Gay*jD7o;%K=q=B_hJo!Mu5gNP^?- z)&UKgYkW_YZao;NyvYxUCV_@OxN|HG!+dUTUC5=Z`r!^&45RPOg%>{B3_U5qb`ydP z7=jawiodWm#Nck>31*r&X?KRjug0c-!`whYPUwVDqNSwREz$U5(NRu)GxXZPNcEqcTY3|<4tyPRRifg_Q|YV+-;7V)z9Z{2t;Ec*VCY;vMH6>N^Ul-ljvHvVsu(A; z;kem2cHb_64*=tPTYm+*EKe96*2O8~F-kOU1pgRFE9(Rd3%TiH)pQW@D2qll*VLr; zba`D#VFe0{n{)Cb!Dvn@#hGE9%edr8mtKEErdr@BZ6SDLx!0Ij#uGFwUG^wmyzjF*mJu~{ODWavGDAFByz1Jjp=r?- ze=^@UtHLH>$_K6Xp(161&c5TXo=F=r>5pZ`|vI0Q_)avipobT)-^zsqSR|Tf3w` z%fNyyE33+D-!G%65iY9PZ4t_+cBAIO7m(4XVpq0TEZ8(bPMm9JnW=3|AeBIzwyrt@ zm1r(jusS91ugV5|OX^yb&rkLCh|E;Wi95i!jcGqSnL>vPBSTjFX$$?_&9WCT36FDcX6Qf1$uFb) zb>wk;bFJ)l(j72KSduh{(ua{3bZ#yU-y;OAh^&ikel2Hq)pkaj1Xvh;cDe50gT5UI z8ul>rf92SYnyVjDN1Xoo>!6}f+`rw_sMiP(UcOXAIwv$rHgS0iekoR3^TE!>BZhDn zs%Femi^#L}=~Xy{|L-nUoF_ zdFe;h%|mfdLg=9un*|QR_itW)0?z6ugS8=p+-|C^FB#BtX?Cth`V7yeU;<9AjbG3@ z8MWFFF>)G-yHg5-v16d4LbhHYgryuW^RwF)lmqTb`lIi31LdjRPjKa68apP0~bONn{g$u&n8qM^Y!wZ%5eHDx-AofkyrCiSY?0O=Ea1ZFbbp4*;t@t z%#juPnS(Xlat0cmJc+7Mh-q%FrD$nT#iIhBd;t%7aRwe@o3v46E+Ae(#eheS%&C6V zMM4Kud`pzHEdR_-$nCIf6ut9QMCyr9?iDxM^Ke{TM5~>F>U+nO7 zqgVXC72`u)IA||9{~kr%xWm-0aPf1LG*>7Z`3f1+-oj0)@yLCiu8ZWQM`M`T0{F;ru3JP z@B|q@{b)~k8Hb4W6$-d!+Uf*~WM6tp1bqF4a8A+c#%8uu8i(IZ*otvC^@=a(NlC1k)ariz;71xPt689IlVT^jU`3>nxZ_iWE7SZ-3&yntWq1 ze!7_$4$jFA0h#_y0KCG0bN(Kl!Z{86gUU+Y>&m!~vj$K){Yzwc%e3(KwOkx2AZLy{ zIHQe@gM3U<#7P++Wg}kS<|E~IPyRXoyDp>;$8A`3k1Sb!OYWCl;*h3HRtBR2_Y~kv$qDfikl$Mh1ici0@Ben zp6f}2A*@?ksmz)5t?U=|ty? zWs1}`CXI)?bfnTdaD3*Ga>=1}Z45n}r-UJoMRpn{A-QxPfqA@3RlnKw^j$lkD(Guq zsew5n+n6NUEA76Q$mc=Zct-X3HiimPu}~i@ayoQN9k#9o*6Y|F4YFz! ziEV&=v%CAvzy7k5R(-(U?L$6Ki_)=2;K^BKQMF?}g#T2{)NuiT zM^r9b$8D8(;tjvZ}Idqr<(DPVvx8r&*^)zg)($vX&odH&l!_CA-yl>#o1$* z(Smge=pzd=NJTa${s~<6-C>7KYfa9qb7u4Y4gpRuiN~yjIYmd=`h{o$CBddesy3~g z;?2oyt%Xi4UX0EnztilD^<|Vw-4;{5?qG)7Qp?Fu(*FdJ45OKdY zr4gxYR;dOR!At!p*Fj{I-cE48*T?UnVxiV8H0>3~4&$Y*d0-SHKVfQ=Hvbz!S6no}2Z91IgK2T4_QS2tH&E_Zfq{Hj?;sMl47Sanw}`~+ghgIPdQ za7xvap@(=}DC?_EB4r-Lmf+!c=aIKf1}hhMxp zq?^F5n3v_Ahyq{I2RVEJj6C}tH7WR+3CWiQRcibYO}mn`Mb_0ROb_oqt*Gg{u-fGU zF0Yd)wNDS_bSPJBM~k96>b_{Sr_2j~F(#Utd?);r6pdv)g9%WLIF3ZE&UL3!di4XE zkQZYSIfryLVP`HCHorO;EeqQ{Vbl{PXC9^r?wpURNSJErqH_JDIW$O1XHGi@vE`Id zhrNdy%#|Frn@O8TLmVB7o}Ay!wz(M=UBnf2+G*c<54B&PI5rxFY%hoUobI((6SBmQ zQYPg(tz&e&aH@7y6FGNRETU({yCajz>gkAsYg=CU%*Lo zhg2SG)mxtI{C)1ld^HGORN{eBE9?s*cDnREpHq<(@L2B`q%|M19qq{{t`(2s-K1Zz!SljHVfVOMub{l5CBM70zLMywlL5xOjq9Bi+oE(=8O z=krb480SZSHo$D0IhcPsC(<9i2|j36FLJHiAAxc@^*u4ltiJ4=?Vs$`3aExA`&-;D z*Jd30{MxbZMHHj3} zUu!Q+!s&kM^-Z|z;h=!Mf~CY$=Y$j2UxyU>f zt2~^5o)Bdbtyk+xJP_XBCjzXsbrp~Com8`97433d2g0yWXhHrTG4f9^y(^$r&Pq!G zz>cq@#{08VT6^ld;rA%f(SmDgy(7Jm6P88*Ol-$D@kDhtr_(x>6-;zHxqvqQ;SA4i zQ?7^FtmTo#y6fiRjFx;2Wt3Ozwo`PMB4#8AwsZl zjC{F;J4IdLqQ$M+65x<{Ym$o=wSDUkygP`kU9hY0d4tp2= z&*OGF^d2tyz5Je&gx1>xm!e%odYMPoO^@ie4|t0Yw(P90t~aDEM?4hG+eubCnVp>x zi%zXSx?8Ub?M`aa`vx3jyr~U7zUC;QTbQnd{no?RC+cxmlEkFI;9<>=?GvjB1CT1g zJ6t2jIf}uqodmA#3Aa%PsL8b5mhS8q39~eYnPn@16>$Mj-Ka-*>9rXhY2uP}w&maN zD%+-%k*XAOoKf~qT(_89)}4&_8xGif=DISj(w%hB+h0!CPQBQl?_cHY(D7-5d+gMI z*5*7;N*(gh`{Z40DxkN81PgDZpvxm3q!Ks-UhRK<7g$oo%=Oq_w^|}h-};a)07-HW zL0qx>wCzTNDqG;HrASVM?u!@>H!MO~MwCM?4ow!P3=kVKWtLUFNpz{tQ9LFt0nH0& zSNehemhtk6<$QYYZh9mK>(D~Kc#uZRBFm3DcdgDuOj954jmlZO|3pgIAEys(k7px%UC-Z82Xts{Zz7f$H25d8d`ktkdJujwZ#x*ij|U z=_T9u=7`a`u9)J#nU@jqS9wdX&mJ|4rtRAqXob%eUsXYoMK8v6Z9L`?7DUdEa1Irk zvwr)dQawg{tTXgKVvB?UsQ2`a{B^we7#s7wwN|>SY;$4;v1yJZBSQSXcGse6*RyqV z`sjEOo=MIc+oryr3-_fx*Rp6B#T0ZM&01BO&UdLdTuHON8I`9m<<;D{=-S~FFMV2*`mkU?l;KJyKKE!yz#m*B{EHB+m&egJ28xqGx_P1;SC0>;P+n! z>v#N@7~^er9)Rx|RTaCxdd56CZD-qze)OR&l(*Go+1zG8&`dm+s7_PPZJaUjfq>+U zc0=38?eqtuSzU|fwaaeZ01}F86z2ow){g`ie2Og{roU*^X}Jpt5p%0PDT)Xv_N|UBHs-es-h)>gf z>CJ&QjIdqd&s(v?F{kjwbNxIw3U$TZB;-pa+GgEx5<}%a7jikt--o*ID|SAurkjz% zON|n(1|xG%Mk)GXFKfNXt#9=E@hp~Wg1AyBo?Ui--dnf0Zo+M9_J8WJP!`r37e}{| z$;d%0ctjs*ZWX{AF6M#|vjy*&99$%^U!vaajP@CP1iAXFyb1qc@kz&0vWL#*D^TMD zkUo(Nl9RruK*84+f`+ZjwVg%o^P-K7yWD2PG%IB^t(J7t>aM>0(j3CpKV(mo(b4gk zccCQ}_Sl#8IJ4VnGOD@va+4gjTq3cXwC=n5d0m0xaf{c!OAqRPC+gXEwq8WrY;pSY zz2g(a`uS$4nLGsB#ReVe_31fdSzuv465Xx-q!YN)fwKyU7m~atin;`c_ol&xf1s=c zft0cd+m!+WJ?YsEPAsDqnzl93c_VC@`ta@*C4SMLYvs@t(th5TQB+cOdFVLfyMcL> z3Wx>PRco5L!jTG&ObS075G&ix+j);S#;uR;@Vz}6->Eyg=r0dA=?GE);7gyMECd~1 zJ=e91_!1sFc^d|WaC@Y3l6cMnZ{o4y=<2^QgHk2IN+rLwN#j=nASYZJ)>S&u6!P9Zmrzk;%|z0P3qBq$8yEL%^$RtU?GFqIVsktWAr^iP&&s+Vb?Fgfxe^-f%$yw$@wLO4 z0sBy#vNoN3(yyp}vL9Z)FDly|nGD6f>%Ox+J6&<5@ZYvId3JidF(^l(yKw+`x@`1o7xUMO-4nYpcq>-`jn7By$b4M=Ai zh%In(NWd;1w9?l$Y5s5uL;}?^Zxdp+NX>4<61*1FQ$f}LQ(VbuITT`d6h1pr-D9?Y z&JSCn59_a*p_LsOn+#viI#!;~ipboJijB#i775*QJ_H{(ISAUnx_{g*c~|T?9Ythw zR5z4-8RdGWpRi=+bN_qUeV%qDL4-d0kCB!fPo2x}uY;XZ>|duaW)Ekn+IF35P~O%*~^AAOFx4$R+J6CnaKiU;`|1_kH41bCsv0BH*Xv3x*gG z%ba4-4TZRq(p3?Ll6*lbTTKn715D=hb`=T+%N?bGFMp_8=QOu-1v)h zJNPOO29jw##F0ffq}{CvdO8}VopTYAAsRQA`9KJc{0BHW2G3w|x3h%B)^dUknt|B2 zlgk3Rx$t7I9)5=P38_4hEZq@-KVKL5@s@^@EjO&&ZqfCTQ>pp#9XN_9{$hSZ_p$J+ zBrjK@=aCm4uLl|$I(zX!Mf&3J5!(upfbLU|!(MbE_I>v4K zCxI)Z5w@Gt`2C8@0=}jUY_fxnd+W@W6U*(JQv%4vws)MKk3|~x{n<;jpeIY#C7P&` zz1i~@o695U>bWO&&}M@er4ZC;)zmS(Mar`|1krnMj8A zIjDNdlkZ<>3%IyKrbtB;9L6bTM$umhSuxti-z%*eM-X^2Re^-D`G+fzJ2xe>S8dn- z$ahWtu8$MsmAq$_c5fO%oOK!8`UMi-YWJ0^GNHH46+vm9gcp4V2*-B?1-zPg#4nHS7Orxjx@oIoE_Qg_BeRQ6DR1Mzi{Z#AO z!zkbSYS-%`t5Mn6kDLA53NA>XOHkzWr%KP=L)?!LSNGff&*NLq&FazTe-31~S2zBI zd$>W;-&p;=7b8xmZc4H3MpTqBUO6DlM(ZLjhREYSIKC?u&Ot%lfE3QQgK? zBevRrkrc|u#a)lGarzxRr>;oS07Fe{O#H_z$K_Ap?ziCtS&GHKOwt0_cf8%$`#bg1 z52EnOFpqG0DuimDu=^TriOZUN%UfisLIidB|J3r1VMT&i@!K+ z)G$yK2@AGeYm@8<=xjOeW;0&<(}JxxKXB=UN%jyB!WtXeM~~%Yja>B5_m}TT()obr z3z@FN(&Fe*tOF+2!%k~+NAFyRN1em1elq5W_u^gzU8_!K{b=ppScf-nQ3xmm>-}z9 z``xqVe)Ln?qz`F~mbf$|^XHa8daTHRiu|Q-l^!&FnS=4W&O_MeZO{Fi1uNs*qxK9h z`^-T8j2xRzPdo~8?}ug$d2G)%Tl=5*?A2VIAAr+{q~1{TxPPg^`A zWU&!R5}|48o$^%nY(f1`g$5$zLzoXg&nk(#Ka4RW>Oky=x~D1t#kXTPggutieZ@++ zj@9BUn5O!(eZH56JNpbf;>RiB6GAk^WoLdz#Y7&A`u1TLppNej6x~2LCr*{nn~wMs zDz%==-a5CFUCB@yPJqj0Z+a8e4IRoLf%9=ihgTb~W#=@1_`Axe3t%LkvCz>OJnRDL zYff|x`4;W4*d7Ui;+4mbJapB01FKPtGFrDu zo@~BF@MC+^JSMSqOo>v4M5*)}bAgf|&+ls({NR2t@kwO&)d8eDMZ?+Z8cs!EJ;U$A9gMNm_!oThChC2F0wtL z(|;HtWfB=Mbs>+OUN7=o(^LK+Fep0eCd4N0#XhNPZxVigB|Xkw8z5!x)5mJ{bSIUL zif>PcdV~pzta*0Uc&iW{8#4ivyxzyaTzSt(Or${pu!s!_NBDTat`#J(>m%*{5w{O7 zDOy2~PNUo!2p80=Z>oF@r2noVYBd_YI_CqvTTNYk|Cfa%27Q!~m3sD+=naAb;4fjl zpJ56LU+(C~Q#UI6?vlgi6Z3_KKZ}A*hf~VTKX@w_n7>PaJYUVnSKfC~i|`ROm%^y< zN#+b|c8UsruLJ$ow0E9;hW@owD$>~LR;6i*-Efu)e}SJs;2%b`)X)_?uO_V7_av&! zJ|AZ?-nENZ4RooF+r&xl;EZ&E6ZB@U-CW6C-Viqs{tBNC9(yYiF8zef`7n1>Z}vV9 zo-8DL_+mI<-pGF7PT*ygRk-mTT4?QX}ACAErvJoG*t+^ z+Fz`&FMe!cb1|0&;z6nS(mRHur^KdQ6P=X{J!G7!Zl-=1irHymK1XbL?#44tx8U^q zTLX5hR*@vN>uT*rj63F$4q!D^W}U){()b&{nj1gd3;3ggaktz`<0iiSY>c0D59oc8 z?N}ifM#9{`-{J>~&IP?E>j^P zbI;a3k}OMjwk*(V$E%z4shZc(dUG`EQF9cdlkc?lCN}Hk(8c-Eg89lQndu$<_&ZaH zD$`#<>ukqDf9ap!{VKuu zOloQyiWKo`9WeNnMcn3BxlJgx;wGt74$x>CdIz%-fB7rn=V%{h>$!pIKRMWwi0Un7$45``TH#?GZAccv_f@n9J2O zvP`gRT}=Hbo7gRT!)s%wsn_SppWCXk(offljT6M))9lN~r6<^l5sh%*5gz&7Fr_`aiW%_GaADuWpZZZp~NfOorqw! z+Dsc0-)gBoMM2jXan^Ikb%`j{zJ9~D_`rr5km&EX7i6Ts+!hSYD-AtFbK}N@-=6UB zKg8ei{pGBGPmQKvko6-5!040=+DENg^r;aq?EPF;fKN54_~C2Wog9DC3rnl@MK}oJ z%DMXte)QqT)JcdWAOfhv&JNOQzRsOZh!}yL7CiN>1F8?24+l?DJ(N-00}r3p;}xDP zfHt$<$QDtK8DuliP$J`2 z#l1NAZlyc`ET#?Ub$CNMkPIYTHx)zc#M7aBDGGK*J8N7OS^aj2&qJY`CF;+QbLuOE zjw&{ULL)*{y6rVp<@aONy0g*in37Lpba{53YERT{?UnYPme?HYu*6JgVD%jIZ1<+M z$@&rlH&dLLNQY}iSh;xStD6ju%Fo-tjx$?1U_-jfY47jt9w%w_cXJ{U>ui5$`h2@6 zDRNgg@m{rU;7Z0u45PoRr5lC=>x%u1tOcmf02t`>P+z#RO_CY92^PL|FXeb(F=s%p zXm&6FV2Fn4S1`$gfFM5s`oi%vg7b>5-y$Y0re5-e4j)5`Qu8ve=FjImP6a=%sEOhUH7eV6$$`b<0R#dhxrC8;gp{b-`~TP4HMrH` z#r>Oawrv|*b+c_QV`;T)+s3l()|MByv~1V1Z+5-6-}^l8U-141=bY!9^VJ72F!a7c zD;jeEI;s0!8O-C0K}AaL96jKH#}Gg+y=B9uz{-;X1=qK)g_YWsh6FWiJvvaH5x>tx z;4nBrNJWTNBqJ{^+TG#wt@J5ok(NU;^{OK2%Q#kreaShMtDw`!KJq|awAD9B#~~U5 zW)NYl4yP+30V)QV-DYPRqbz-Z#KTS%hrqdO0guR8*-#o4{i`u*jXEpoC-nJ2&j|?qwx+;`d7BU#y?#&fx!^T^BruRjC zPhA0meO-JOS;RN_xi0S1{AlLw>vbWkg?^CA+7TRF_&_*u;pu>G9r!hJBEtygt8o^Y+eRp<+9grNXY}g2i0vi6a%L9f zINX4@i#t;1acVi`LuG~yrLnRXpVU*I3jsW0Jq+AWLtU?}p}j zdJX@5B>UkM1O*%5Oihu%mAapEe%Pbh%07N*ju)^g`Q$dbV7M}7d<73z zh>R|d9UJxad~Y2~kK7Sn_Eo%h2Fk~6)2R9Z9D6390(z^Rh*8st#Fz50j>Q!WhFQyNH%|KQxEj4~=~qdeRrG!JIMNFyFXaUl3Tn3HgSB_EfRSD^`l~ z8iTeNXuKWlA?1%OL4=FKQ9E+b_HAHfq>%u|okq3^pE~=Hq5^lE&5?ry8$6wonBY9* z;@LQSf8Q9IbMo!E4Git{8hx6q{nvh(Q|X-&^n*F~*@aSkJcnC?9qt^N(V|AS$hgo@ z_`s&!zPrfhr0wgf;Q8Bu4{_bPgH6KCshIim&$9$xNfb~h4Qx<%-1`H2;SwKQW zXnBim<_KKlXhaV>Iex63idzgs0;TQcQkbOkqzvapv0GxnStgHfWWRhdEYuW-PQ6bC zHt%jc_p(t_XZ^PAs{<5ByaqK&FwoQ0Q>UDyOL@-))X92B^v^}VaI-f~L#9w8?XSwR zoCR&5xSBNHj#>w_-1>Jz1XiWw1}TLmvXLw^K-mvMzGU#1xIn9dSOziWS)ZR%norcs zhbm(xb1$Af+GYg~GZcdgRiPU-M=sz0_EAb*UK1jN&KQy4cd<^O-8Eq5RitKOzciaF z+{McKQQTn@hnxetX87y}u)8aLE^aD+AucPHriq6eQ zvkV8oh+xgnCo)&77{wrChH9+IRq!jp#3B{UhnC>33H=<_NbXYOgPi-EXo79er^?_1 z!pl4kG_}H-vQHa@XIGlNH<9yWws`IaK6Bkt4wZhznL2@TTW zuOlo?S;$1&OIc17iSAR!=KSXR<#UHOepBQW1i2KRIm4osga6@`GdY92~n>jNG`i|7xrc_w)Rh+^;x}M zD@^)U{bP340Sfv6J;Suj~WdY|O;2#}CB?o>cTNtNx&Z)@Uj(P>>4BiAA3Lx>uBsH)Tf2%23j4h&@ zyrOwT)ZV5vA-HYiip5>Wfk!gOC1n)h!<0!cvgc(~c!%RU zB=D-O`FZgpjG)ju#@jkgtY&aG zHAWiJ8kPJ~{tK2t&YB^{!$b94%cb4mF5vmt#Qefp?8ZPA$jcdvUx9aNL^u{D(a|=B zC9XVPKArS2leFE;ok2FT9~C{q4T}Sp8cq<#Ky+XmD2*5wW=h`57OQSuQY6iMWJz1_ zH&&v0Q-l2l9m2_a<@OfWP!0O`nK`zY4CAoQch2Fdn^ZCL2Zp(nnnV4V-Kt0#8;UO< z83&V~KNXhi*Y2lcnI9qFr(rJ6a2-SJCPqEr89aX-Pic+{J6q0@;3a3DN3h8^M9CK3 zW{>#9Wp*{}4UwH9(dBbCZ}u@@w*Sk~l*6#Y-E~nuHNS2rAdJPxE!Hr=c0=Z;>l0}( zk_*0|j-?5bU5pSRLVpt9PPJ$Sev$~m&wLwg*ZC}y`i>Fi{hMNnaA8c;En6r|^kCps zM85YNtah&&GS=iLjIugz>N(i`mefeZ=PW&Tw0UCY@@|M6mr z19G5&=$bR_BH_M#hiMBtqF-gdsN0NdJ0hO&T&-1-Xy zF})`dapblza&`!nyZKTvi&Ihb$2ze7s9G(ukjIMbrT4ZvOOwW7%d~`off?skN}^{H zCge4o07O^P8C}cARPpJl;P5}fhTBBlBPvnJX8~(~$fF#?$^JgPL*@7J)q-$Rq1Y)6 zc9q**A)DR?!y;C{1OGL-qA&5@$~4yrZyHtc7(txD|MtaekVTo_C)oMqFizwnr#i70 zaQzVuD?c|~x(^;Fo}c$~JN@@Sd{D|NDM*)evfhrS+`>m&jLzbhy{$})q3}kkkTv^* zpuiY$G$UCFH?Od9;)u7C--B_XEg!`veS4Fbe;(^yqTD*9*!Dn}D{3O?L-dKM4ee5h z|KbVoKQ;j8yiYolKh^2cnR%;5kd|Cnd0D-Bl7?V2(!7fo%?$=9-|%IP+ixJa5$t1H2Jz;71#fjMsYpHS=o z!9{2PX&wz(#++M2_G3TjZf2gN_l{%mh`&H{#e?N@s}S~Eaj;C@+DH=n`N_{k z8aQz{aQhOOL?@I%*BX~U0*tCVe)`(BQf*e4---LIMgr#!qFS;hi8bw znE^v|!+A?7>`J$)QNMiZvN*L#0&aKBI~)5_g3%Brn0d5QQBQqMs%X4-21OnWRujuT z7<6U$^#^-xy>q2GVS@l7#n7U-Qi0)C9!VBL@U>{>zi~b~{9Ybj)H;P|r9hHU+BY7c z)Z#~&%{7g@-lHnM&_>#9F%Py~bvcH5%8Rv6N=%>2C@_mHHbLrvj|0n#%KUrjPa4Vt zQ8J(Vh2=ekA5DJ+%s-uWORsaIXFGxUNv;b4Uw$WUjRoxn3m~lOFt9UXs{BGB6t)c) zL3?Glc2oXH#eXuu+*ev6%l})`SXaHXR&UF|*#JzQQXCyXCgje~$eO+#GBiOIE#PW& zL=Mrs%ApU%56id_iNpEFt-~e999Z5JPqI~39-S9NF2n${U#++4R_>xq2_*uzOqM=v zJrjj7B8t%_(arKtC~80LQ#N|I0`;IFK}STJp~gDL;1eRTS_ec*6Z&`93ZPxr!;T7HWKs*dAPt*6Pwe%}`x?OWu;PnA@ovKkq21hCMk9)u{Rwq<*8IqCuCs zSU2?Xig|>DM!mIGAzoN~>ck!9S}BhYzWs*Lq^qZ&_~STjDgQpzB>v}dPlVTkg-XV_tV|qS_P8A&Y6%m`3JtKRa}7Dr4EN^ z59nfOCZ#h2r?l1UU|4QFu(v;qQtBUkwbwEz7(q}QWIRjl&}@X2Cj=K?G$(|H$zQkZ zO~Y228zTWbcv>g=5I*x8mp#O-;?+O#RtR%RCA0SpNi2F07xUYfWIZH>?GBUb3aS6D z!12Y;bj-a1%DVwov4k@lBKPDwmNS`KaYigZBwS&@igz^x5``mpQ=94fvjdn<#|~g&HEf35NB996l*Ez+PIOz!GqDxV zU~?u)axOVQ3?SL*7LgQ>@o7yQ9=s>Q;Ona$<#EHatXDCcKb`4;vPfMYiOSpb(i%Q( zCt~->$;V3L%`ZcqmL9!FF8i)lu=M?>_m2|8Z~a!m}6kxlkAm_mm?> z4c^wjJ>v3z^(D`^=PKEGBaOeWJ}($APOW)5Z55sVrMiJ;Kb<6Fl=va~gfS?uP+dYg z`U3}kT+V8<1@|keSB;K2#+}fBKd-E8tDP3}z`hN?%-j6*JV!gUkD>hgyhtrTAer%)aL)!89J{o>*Pxy72w#Xt=Oj?=Pr?LWe!fTG_GO3!e;7R5s5T6*#pwq!+kRUlr!|)% z$h2taGyH2QDTbNxU~3*u!a{x7#dR8_cp-FMkYQpVZI=zEI!y{L7vC%WAXyA=r#1IA z0JpNAlEwFPfweW2&@kU|0hP5WC3u_-r2ivsDNgmlK96IrB=$Y8;UU+~=}9R8QBsS3 zeM@;ddtxaGGL(>a_jVh_dJwui7~t5tnZJMNZayj~!!LZkiNDBPmaIsTXxmPze+HTM zUU$#_L5T_vY35$>KjtGOI=B;&pTnY`;|oXSNe0#A&x~1pk_@~veLYk#FqVbTtJ~T4Y+|u{s7|&d_jtvtmg@UiK^xb z(aJ(c7X83ld2kiDA%;jwY&D=Qj449X#f?zR0KE3|_?mH%dhc_v;3D4gIZJ(m_f)Gm zGOz>51D+FPI!3WRxoyiI-WUwR@Y)K{c!}ROT5IZvnXpE1>X0qG8k*wvBgmSaXDRVk zHgf@XSWd)-x^|aPZ#?Kb&bpYfZrD&`4fXk$H=(11+}P9zB<^_owltjAsh40~%Hpeu z=bZC%c4FK$@!LRDhai`?9UZ9;`VnMMowV$r@S;##Ph6`SSMop+B$#Ar6&Ar@MO`7j z-1Vr2>2TfR-dC3xIwMcz zaYC$@Ks7VyYs!>kq#sw9t&R@&_vMpf&~Vn@kVHc0)!p(^%F4jmO1#nsorx z-^$jJC4R>|B3kC7qAtbF>Cx@JH%4~Bsm=?sjJf#USZLnVH+FtTqSF|+U;_b>(v}Sz z)X;Mxbak?a!S9S{Y@HSTPoFH+m?kxo-yEvI z#pG@aW>2jEv-K5~OQ$;}QRnXoS~%hDV3u@H#;#rCktABna=I6&6#WDdiR6QYk1Tl? zoSvdP9^Rha;U2nkCKES$sEHnS9;V<{j+5WpX33r?H~O$A{gY{e1f*m-#fTPJozzD_ zvD&xuluQT@w}AvBUksrWQ8hK)3}|X}oL=bwYT%iq-JkT+nZQBOhz_}4XMGYRd+kji z8yC$W4{evJYq>poP%?k$)T0a+^TQ*mu}L!Q=AhYGb>g^?3Qcr0Tv~9b0+NtX#%<_& z+!1yDwb@F3xm6~t;ZU298Dxa<41mpB(JS2I?ViX9uU-BHQ9#3aLCll@fuuXu|0n7#ddCP|{? zc#K@DiX5s9I%;Tj*uSqH<4#UtFsn31TmVUE@Sx;$m-tCO$^l_UD2o99Gp*Bl^%=;4 zs{2s=t}X9Id&(svr=Ci z#O8kg&%TF4*Q80DvMr(6kg`Qj`u^dvNK49^jW}bn{}FFiPoG1VpmdN8^QqE%aUyQk zeInHyhz&zY(GgjAQiG8Q>o<#0Tb$^)k(=A0{|S!FU$z%9vwdc#0X7nOqH%b z;c>@v7G5O{-A6)P!t+#e;=SfotJX2`SIqQ!k0%Eav|pEMc6VKM!@e8PsK zZOhB=<#U=iVuhMEC0;gi6z<9L`bQ%xjg9Z5@Xu9fP{N;9Yi0HG26l`5Vx#%`(#p5W zpWqOur8JInZYpJm4EZ%DYIGZGIJ(jb$UA~ax<(sgr-$V;i8N_S4AcZh8-L>a`zhu4 zJTLKqh6qjlbI6y>GRZw&U1p1N`z8$9$&=9NDL&&uCdDoIB+@wkLe5zVFZiu)1x33S3Dgkg_CKgno>Q3 z{+eFgVF<{|YG-)xirBkfH09~Yq4vdZAu$qU;sc5%q=(k!np9@&SD$=u2|DXG7&F2$ z=0uW5saT%N14}PT{B=%C*J=BXu7#m4xWF3S{kG}?|AqPqGbbHr5m4e#_HOe@ae$oC z3=4mlitMPoErD3A2}5Bo5_0APai>=E1JfXuIu%a~(DHItGqUY^df2#l(xf*RHv{s5 zUs`6oSfVo}6t%t*vbe(xTbec$k7hnXMMRZ=(a*54*K4f#VL7;>1IWx%WmB*zJ zA%X0=BPqUJG?Z;7b%DazF9qpp{u8}jL!J+B3-6&OXEqfj8&6hb!n&&UwTW-!8Jn}B z9j;yk7Q@tQFbcL86UxjWwtBwm=MVrO?oI$U;bsTmb}h>v=`eA+gB!gSz1&AnUAi3_ ziXU?Wbc{Z(>T;nMf* z&JJ-Tc*8!?1>LeCiv2M9E%IRng{#@{u1)6n-nw18_~AzQJyR8%ref=N;L#gQQ`9q14hz+qC{oR&0h z6^5@yv{AzNex!^)Har)~w96Avl;QcAtimrh6`e+Y4R#(o;HnE>H;5-NeDRr3$JO&1 zqT30S)tv?TD2p4U{c360eIZDDl`PfD!VJD!&u|ZY+Nfys|h+g!U>P@7RNdXEh@JQNSnm))uvC&_P`Oe z@lj(iDj9fQWyyxc6B=8b^F$x7OMP$i&0WP2&=teUK#9)OagjS_ZTI#CHoJ1* zX$9KU6=PcHy5VDjFLvSnBJdpd{K`mWiacb?jGi-fa(dd)^cY!zFq}mBKCF2^Lyg`>GFocds&~}5UlXUy9QoVL$s7K>uSN9 zRE()q+xKBNzdc*3SaHQilGr7GROsr-i@nT6e@l5IwRtejwVv{x=gidU%eTR0$F*I< z(KOrOnC9pk*jv~oGH*>D3Aku*Z8X*PB4YtMu+WHsr|jdG1f0n0Sz?XJh?(o(_(ppG z0Nk&4j{x>mZqwiw&Qpwau;&p<>$*hz{WZx6cd)Vf9TZIIQVzinklPPA?04M|Y>_~$ zR?v{z?cxF1TLlR|`FR(0!odl#^=1l|l-k>*h;Yhvp1kYX+x|i(+bm=#YW5vavHq$$ zPl|M5;DjFIwV5JA#}whNdhX5(V<8#HKyjWhX1kYQ!**-kMUj82M$Df4443})6II{8J@--*7;e+9iBm+>)a9`onD7e`@aVFBkmMQ10E3D^4Hw8&x8ifD|g)UFb@^ zmEZ@dfdH_Tl;!q~#tq-(tgH6K0Iy&(kPet0*qKyEjgs&dQ-Jst$bjTxt@&CLm=61x zD=sgkc19niN%T!ZlH*H+-6RZ`9GFMNhA3@CE9PCZ#*9}>Xt1$Sg^7qR^6W4`f*Nk% zg(7Z+hpTI_@9bj_@w)bmmLzPT7|;XwDHN4B^Nc2Ge)ltR=|2(C${xl#P^EeJ?G`C6 zo>WZcu#P=DLqGXjo`N6@Ri48~V`e$By(S>kSIzSi>wcv4l8eyrRvA)|K#wNk+h$KN zU8Z!Dtrmo>53x}wg6Er7N=iiw^+ezEpXc_xT^|<)VW&nHWp4fvvES+Mfy+N91m2M4 zJawS)n{Lt9pC==-=ePSDo4mFPh5S6tG6lXoy>;+^`0MjBKl^0MgULej?d8iWF_%02Y$bhij-3R54NBTTbl3|i;v+|?X)=-E9X zl`Lcm((rsjau4?dr_pTowj&lfmOuBf9&Z?1E=Oh-Zqx$`vsAngG!y)9&cVi9Zsd zvAO`M+LMe!Toe00%aMjfqQIfw#6|Sy%Ok|KAs)J%Jc>A4^t*kwJ_(DLcyTB=W{o=5 z*#EnNW_Ui8j2GA=Hv*9VO8PnhWc<`_KvLiZALC8lO3>rC)%F5$wa6fH;m6(mxU%)L zJ{|I_K$!D^jZx{(;4G}qoitdx*wgtln$$`x-z68!Q?mctnLe+hcNP%CoVKb9G_0PR zMWys^fg*}%4LC>Cu99u(#nMe rtf.Created.After.Unix() + if f.Created.After != nil { + createdAfter = creationTime.Unix() > f.Created.After.Unix() } createdBefore := true - if rtf.Created.Before != nil { - createdBefore = creationTime.Unix() < rtf.Created.Before.Unix() + if f.Created.Before != nil { + createdBefore = creationTime.Unix() < f.Created.Before.Unix() } return createdAfter && createdBefore } -// matches checks whether a resource matches the filter criteria. -func (f Filter) matches(r *Resource) bool { - resTypeFilters, found := f.Cfg[r.Type] +// Match checks whether a resource matches the filter criteria. +func (f Filter) Match(r *Resource) bool { + resTypeFilters, found := f[r.Type] if !found { return false } @@ -170,10 +242,14 @@ func (f Filter) matches(r *Resource) bool { } for _, rtf := range resTypeFilters { - if rtf.matchTags(r.Tags) && rtf.matchID(r.ID) && rtf.matchCreated(r.Created) { + if rtf.MatchTagged(r.Tags) && + rtf.MatchTags(r.Tags) && + rtf.matchID(r.ID) && + rtf.matchCreated(r.Created) { return true } } + return false } diff --git a/resource/filter_test.go b/resource/filter_test.go index 659895e41..ccac01415 100644 --- a/resource/filter_test.go +++ b/resource/filter_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/cloudetc/awsweeper/resource" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,12 +16,10 @@ import ( func TestYamlFilter_Validate(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.IamRole: {}, - resource.SecurityGroup: {}, - resource.Instance: {}, - resource.Vpc: {}, - }, + resource.IamRole: {}, + resource.SecurityGroup: {}, + resource.Instance: {}, + resource.Vpc: {}, } // when @@ -31,9 +31,7 @@ func TestYamlFilter_Validate(t *testing.T) { func TestYamlFilter_Validate_EmptyConfig(t *testing.T) { // given - f := &resource.Filter{ - Cfg: resource.Config{}, - } + f := &resource.Filter{} // when err := f.Validate() @@ -45,26 +43,22 @@ func TestYamlFilter_Validate_EmptyConfig(t *testing.T) { func TestYamlFilter_Validate_UnsupportedType(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: {}, - "not_supported_type": {}, - }, + resource.Instance: {}, + "not_supported_type": {}, } // when err := f.Validate() // then - assert.EqualError(t, err, "unsupported resource type found in yaml config: not_supported_type") + assert.EqualError(t, err, "unsupported resource type: not_supported_type") } func TestYamlFilter_Types(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: {}, - resource.Vpc: {}, - }, + resource.Instance: {}, + resource.Vpc: {}, } // when @@ -79,10 +73,8 @@ func TestYamlFilter_Types(t *testing.T) { func TestYamlFilter_Types_DependencyOrder(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Subnet: {}, - resource.Vpc: {}, - }, + resource.Subnet: {}, + resource.Vpc: {}, } // when @@ -104,7 +96,7 @@ func Test_ParseFile(t *testing.T) { created: before: 23h`) - var cfg resource.Config + var cfg resource.Filter err := yaml.UnmarshalStrict(input, &cfg) require.NoError(t, err) require.NotNil(t, cfg[resource.Instance]) @@ -125,3 +117,281 @@ func Test_ParseFile(t *testing.T) { assert.True(t, cfg[resource.Instance][1].Created.Before.After(time.Now().UTC().Add(-24*time.Hour))) require.Nil(t, cfg[resource.Instance][1].Created.After) } + +func TestTypeFilter_MatchTagged(t *testing.T) { + tests := []struct { + name string + filter resource.TypeFilter + tags map[string]string + want bool + }{ + { + name: "no tagged filter, resource has tags", + filter: resource.TypeFilter{}, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "no tagged filter, resource has no tags", + filter: resource.TypeFilter{}, + want: true, + }, + { + name: "filter tagged resources, resource has tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(true), + }, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "filter tagged resources, resource has no tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(true), + }, + want: false, + }, + { + name: "filter untagged resources, resource has tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(false), + }, + tags: map[string]string{"foo": "bar"}, + want: false, + }, + { + name: "filter untagged resources, resource has no tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(false), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.MatchTagged(tt.tags); got != tt.want { + t.Errorf("MatchTagged() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTypeFilter_MatchTags(t *testing.T) { + tests := []struct { + name string + filter resource.TypeFilter + tags map[string]string + want bool + }{ + { + name: "no tags filter, resources has no tags", + filter: resource.TypeFilter{}, + want: true, + }, + { + name: "no tags filter, resources has tags", + filter: resource.TypeFilter{}, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "filter one tag, resource has no tags", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + want: false, + }, + { + name: "filter one tag, resource tags have no matching key", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foz": "bar"}, + want: false, + }, + { + name: "filter one tag, one resource tag's key matches, but not value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, + }, + }, + tags: map[string]string{"foo": "baz"}, + want: false, + }, + { + name: "filter one tag, resource tag's key and value match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "filter one tag, one out of multiple resource tag's key and value match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: true, + }, + { + name: "filter multiple tags, all match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + "boo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: true, + }, + { + name: "filter multiple tags, one doesn't match (key)", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + "boo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boz": "baz"}, + want: false, + }, + { + name: "filter multiple tags, one doesn't match (value)", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + "boo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "boz"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.MatchTags(tt.tags); got != tt.want { + t.Errorf("MatchTags() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTypeFilter_MatchNoTags(t *testing.T) { + tests := []struct { + name string + filter resource.TypeFilter + tags map[string]string + want bool + }{ + { + name: "no notags filter, resource has no tags", + filter: resource.TypeFilter{}, + want: true, + }, + { + name: "no notags filter, resource has tags", + filter: resource.TypeFilter{}, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "resource has no tags", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + want: true, + }, + { + name: "no matching key", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foz": "bar"}, + want: true, + }, + { + name: "matching key, but not value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^bar"}, + }, + }, + tags: map[string]string{"foo": "baz"}, + want: true, + }, + { + name: "matching key and value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: false, + }, + { + name: "matching key and value, multiple tags", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: false, + }, + { + name: "multiple filter match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + "NOT(boo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: false, + }, + { + name: "one of multiple filter rules doesn't match key", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + "NOT(boo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boz": "baz"}, + want: true, + }, + { + name: "one of multiple filter rules doesn't match value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + "NOT(boo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "boz"}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.MatchTags(tt.tags); got != tt.want { + t.Errorf("MatchTags() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/resource/select.go b/resource/select.go index d534ed422..c430eaf34 100644 --- a/resource/select.go +++ b/resource/select.go @@ -37,7 +37,7 @@ func (f Filter) defaultFilter(res Resources) []Resources { result := Resources{} for _, r := range res { - if f.matches(r) { + if f.Match(r) { result = append(result, r) } } @@ -49,7 +49,7 @@ func (f Filter) efsFileSystemFilter(res Resources, raw interface{}, c *AWS) []Re resultMt := Resources{} for _, r := range res { - if f.matches(&Resource{Type: r.Type, ID: *raw.([]*efs.FileSystemDescription)[0].Name}) { + if f.Match(&Resource{Type: r.Type, ID: *raw.([]*efs.FileSystemDescription)[0].Name}) { res, err := c.DescribeMountTargets(&efs.DescribeMountTargetsInput{ FileSystemId: &r.ID, }) @@ -74,7 +74,7 @@ func (f Filter) iamUserFilter(res Resources, c *AWS) []Resources { resultUserPol := Resources{} for _, r := range res { - if f.matches(r) { + if f.Match(r) { // list inline policies, delete with "aws_iam_user_policy" delete routine ups, err := c.ListUserPolicies(&iam.ListUserPoliciesInput{ UserName: &r.ID, @@ -116,7 +116,7 @@ func (f Filter) iamPolicyFilter(res Resources, raw interface{}, c *AWS) []Resour resultAtt := Resources{} for i, r := range res { - if f.matches(r) { + if f.Match(r) { es, err := c.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ PolicyArn: &r.ID, }) @@ -161,7 +161,7 @@ func (f Filter) kmsKeysFilter(res Resources, c *AWS) []Resources { result := Resources{} for _, r := range res { - if f.matches(r) { + if f.Match(r) { req, res := c.DescribeKeyRequest(&kms.DescribeKeyInput{ KeyId: aws.String(r.ID), }) @@ -184,7 +184,7 @@ func (f Filter) kmsKeyAliasFilter(res Resources) []Resources { result := Resources{} for _, r := range res { - if f.matches(r) && !strings.HasPrefix(r.ID, "alias/aws/") { + if f.Match(r) && !strings.HasPrefix(r.ID, "alias/aws/") { result = append(result, r) } } diff --git a/resource/select_test.go b/resource/select_test.go index e4332c95e..0317e20d5 100644 --- a/resource/select_test.go +++ b/resource/select_test.go @@ -14,9 +14,8 @@ import ( func TestYamlFilter_Apply_EmptyConfig(t *testing.T) { //given - f := &resource.Filter{ - Cfg: resource.Config{}, - } + f := &resource.Filter{} + res := []*resource.Resource{ { Type: resource.Instance, @@ -34,9 +33,7 @@ func TestYamlFilter_Apply_EmptyConfig(t *testing.T) { func TestYamlFilter_Apply_FilterAll(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: {}, - }, + resource.Instance: {}, } res := []*resource.Resource{ { @@ -56,11 +53,9 @@ func TestYamlFilter_Apply_FilterAll(t *testing.T) { func TestYamlFilter_Apply_FilterByID(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^select"}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^select"}, }, }, } @@ -87,12 +82,10 @@ func TestYamlFilter_Apply_FilterByID(t *testing.T) { func TestYamlFilter_Apply_FilterByTag(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - }, + resource.Instance: { + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, }, }, }, @@ -130,13 +123,11 @@ func TestYamlFilter_Apply_FilterByTag(t *testing.T) { func TestYamlFilter_Apply_FilterByMultipleTags(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - "bla": {Pattern: "^blub"}, - }, + resource.Instance: { + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, + "bla": {Pattern: "^blub"}, }, }, }, @@ -171,13 +162,11 @@ func TestYamlFilter_Apply_FilterByMultipleTags(t *testing.T) { func TestYamlFilter_Apply_FilterByIDandTag(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^foo"}, - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^foo"}, + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, }, }, }, @@ -215,13 +204,11 @@ func TestYamlFilter_Apply_FilterByIDandTag(t *testing.T) { func TestYamlFilter_Apply_Created(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Created: &resource.Created{ - After: &resource.CreatedTime{Time: time.Date(2018, 11, 17, 0, 0, 0, 0, time.UTC)}, - Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, - }, + resource.Instance: { + { + Created: &resource.Created{ + After: &resource.CreatedTime{Time: time.Date(2018, 11, 17, 0, 0, 0, 0, time.UTC)}, + Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, }, }, }, @@ -265,12 +252,10 @@ func TestYamlFilter_Apply_Created(t *testing.T) { func TestYamlFilter_Apply_CreatedBefore(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Created: &resource.Created{ - Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, - }, + resource.Instance: { + { + Created: &resource.Created{ + Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, }, }, }, @@ -304,12 +289,10 @@ func TestYamlFilter_Apply_CreatedBefore(t *testing.T) { func TestYamlFilter_Apply_CreatedAfter(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Created: &resource.Created{ - After: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, - }, + resource.Instance: { + { + Created: &resource.Created{ + After: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, }, }, }, @@ -343,15 +326,13 @@ func TestYamlFilter_Apply_CreatedAfter(t *testing.T) { func TestYamlFilter_Apply_MultipleFiltersPerResourceType(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^select"}, - }, - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^select"}, + }, + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, }, }, }, @@ -393,15 +374,13 @@ func TestYamlFilter_Apply_MultipleFiltersPerResourceType(t *testing.T) { func TestYamlFilter_Apply_NegatedStringFilter(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^select", Negate: true}, - }, - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar", Negate: true}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^select", Negate: true}, + }, + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar", Negate: true}, }, }, },