volume_sweeper 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +220 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.standard.yml +3 -0
- data/Dockerfile +20 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/lib/volume_sweeper/cli.rb +57 -0
- data/lib/volume_sweeper/comparer.rb +56 -0
- data/lib/volume_sweeper/core.rb +73 -0
- data/lib/volume_sweeper/kube/client.rb +138 -0
- data/lib/volume_sweeper/metrics/controller.rb +41 -0
- data/lib/volume_sweeper/metrics/prometheus.rb +4 -0
- data/lib/volume_sweeper/providers/aws.rb +39 -0
- data/lib/volume_sweeper/providers/base.rb +23 -0
- data/lib/volume_sweeper/providers/oci.rb +124 -0
- data/lib/volume_sweeper/utils/log.rb +19 -0
- data/lib/volume_sweeper/utils/notification.rb +148 -0
- data/lib/volume_sweeper/utils/notification_formatter.rb +47 -0
- data/lib/volume_sweeper/version.rb +3 -0
- data/lib/volume_sweeper.rb +6 -0
- metadata +267 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3b347010ebacef693dd8fe9917e0ef3165dadeed8f79076e61ab5fa051ae6f6a
|
4
|
+
data.tar.gz: 613ff41df273c38ae7b8a3c55176c284716aa64be804b1fd86b4d824cbdfff90
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ac1b14b8a25b824a0832b423a20ba0ff8d00653f70eebb83d6e266fced137096b80f8ce82c5496eeffa3f967d194b77a32f72641411c97bd10b14a8f35cb523
|
7
|
+
data.tar.gz: 8f3212d22053ba1c3ed803dd659929f635e7b4ac7c73f618263ce27a3bc83439fd72f47f358344c5e276d59be817f29f6f5b73cf1bb2d2bb5aee96816e1793ba
|
data/.dockerignore
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
.git
|
2
|
+
.gitignore
|
3
|
+
|
4
|
+
### Git ###
|
5
|
+
|
6
|
+
# $ git config --global mergetool.keepBackup false
|
7
|
+
*.orig
|
8
|
+
|
9
|
+
|
10
|
+
*.BACKUP.*
|
11
|
+
*.BASE.*
|
12
|
+
*.LOCAL.*
|
13
|
+
*.REMOTE.*
|
14
|
+
*_BACKUP_*.txt
|
15
|
+
*_BASE_*.txt
|
16
|
+
*_LOCAL_*.txt
|
17
|
+
*_REMOTE_*.txt
|
18
|
+
|
19
|
+
### JetBrains+all ###
|
20
|
+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
21
|
+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
22
|
+
|
23
|
+
# User-specific stuff
|
24
|
+
.idea/**/workspace.xml
|
25
|
+
.idea/**/tasks.xml
|
26
|
+
.idea/**/usage.statistics.xml
|
27
|
+
.idea/**/dictionaries
|
28
|
+
.idea/**/shelf
|
29
|
+
|
30
|
+
# Generated files
|
31
|
+
.idea/**/contentModel.xml
|
32
|
+
|
33
|
+
# Sensitive or high-churn files
|
34
|
+
.idea/**/dataSources/
|
35
|
+
.idea/**/dataSources.ids
|
36
|
+
.idea/**/dataSources.local.xml
|
37
|
+
.idea/**/sqlDataSources.xml
|
38
|
+
.idea/**/dynamic.xml
|
39
|
+
.idea/**/uiDesigner.xml
|
40
|
+
.idea/**/dbnavigator.xml
|
41
|
+
|
42
|
+
# Gradle
|
43
|
+
.idea/**/gradle.xml
|
44
|
+
.idea/**/libraries
|
45
|
+
|
46
|
+
# Gradle and Maven with auto-import
|
47
|
+
# When using Gradle or Maven with auto-import, you should exclude module files,
|
48
|
+
# since they will be recreated, and may cause churn. Uncomment if using
|
49
|
+
# auto-import.
|
50
|
+
# .idea/modules.xml
|
51
|
+
# .idea/*.iml
|
52
|
+
# .idea/modules
|
53
|
+
# *.iml
|
54
|
+
# *.ipr
|
55
|
+
|
56
|
+
# CMake
|
57
|
+
cmake-build-*/
|
58
|
+
|
59
|
+
# Mongo Explorer plugin
|
60
|
+
.idea/**/mongoSettings.xml
|
61
|
+
|
62
|
+
# File-based project format
|
63
|
+
*.iws
|
64
|
+
|
65
|
+
# IntelliJ
|
66
|
+
out/
|
67
|
+
|
68
|
+
# mpeltonen/sbt-idea plugin
|
69
|
+
.idea_modules/
|
70
|
+
|
71
|
+
# JIRA plugin
|
72
|
+
atlassian-ide-plugin.xml
|
73
|
+
|
74
|
+
# Cursive Clojure plugin
|
75
|
+
.idea/replstate.xml
|
76
|
+
|
77
|
+
# Crashlytics plugin (for Android Studio and IntelliJ)
|
78
|
+
com_crashlytics_export_strings.xml
|
79
|
+
crashlytics.properties
|
80
|
+
crashlytics-build.properties
|
81
|
+
fabric.properties
|
82
|
+
|
83
|
+
# Editor-based Rest Client
|
84
|
+
.idea/httpRequests
|
85
|
+
|
86
|
+
# Android studio 3.1+ serialized cache file
|
87
|
+
.idea/caches/build_file_checksums.ser
|
88
|
+
|
89
|
+
### JetBrains+all Patch ###
|
90
|
+
# Ignores the whole .idea folder and all .iml files
|
91
|
+
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
92
|
+
|
93
|
+
.idea/
|
94
|
+
|
95
|
+
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
96
|
+
|
97
|
+
*.iml
|
98
|
+
modules.xml
|
99
|
+
.idea/misc.xml
|
100
|
+
*.ipr
|
101
|
+
|
102
|
+
# Sonarlint plugin
|
103
|
+
.idea/sonarlint
|
104
|
+
|
105
|
+
### Rails ###
|
106
|
+
*.rbc
|
107
|
+
capybara-*.html
|
108
|
+
.rspec
|
109
|
+
/db/*.sqlite3
|
110
|
+
/db/*.sqlite3-journal
|
111
|
+
/public/system
|
112
|
+
/coverage/
|
113
|
+
/spec/tmp
|
114
|
+
rerun.txt
|
115
|
+
pickle-email-*.html
|
116
|
+
|
117
|
+
# Ignore all logfiles and tempfiles.
|
118
|
+
/log/*
|
119
|
+
/tmp/*
|
120
|
+
!/log/.keep
|
121
|
+
!/tmp/.keep
|
122
|
+
|
123
|
+
# TODO Comment out this rule if you are OK with secrets being uploaded to the repo
|
124
|
+
config/initializers/secret_token.rb
|
125
|
+
config/master.key
|
126
|
+
|
127
|
+
# Only include if you have production secrets in this file, which is no longer a Rails default
|
128
|
+
# config/secrets.yml
|
129
|
+
|
130
|
+
# dotenv
|
131
|
+
# TODO Comment out this rule if environment variables can be committed
|
132
|
+
.env
|
133
|
+
|
134
|
+
## Environment normalization:
|
135
|
+
/.bundle
|
136
|
+
/vendor/bundle
|
137
|
+
|
138
|
+
# these should all be checked in to normalize the environment:
|
139
|
+
# Gemfile.lock, .ruby-version, .ruby-gemset
|
140
|
+
|
141
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
142
|
+
.rvmrc
|
143
|
+
|
144
|
+
# if using bower-rails ignore default bower_components path bower.json files
|
145
|
+
/vendor/assets/bower_components
|
146
|
+
*.bowerrc
|
147
|
+
bower.json
|
148
|
+
|
149
|
+
# Ignore pow environment settings
|
150
|
+
.powenv
|
151
|
+
|
152
|
+
# Ignore Byebug command history file.
|
153
|
+
.byebug_history
|
154
|
+
|
155
|
+
# Ignore node_modules
|
156
|
+
node_modules/
|
157
|
+
|
158
|
+
# Ignore precompiled javascript packs
|
159
|
+
/public/packs
|
160
|
+
/public/packs-test
|
161
|
+
/public/assets
|
162
|
+
|
163
|
+
# Ignore yarn files
|
164
|
+
/yarn-error.log
|
165
|
+
yarn-debug.log*
|
166
|
+
.yarn-integrity
|
167
|
+
|
168
|
+
# Ignore uploaded files in development
|
169
|
+
/storage/*
|
170
|
+
!/storage/.keep
|
171
|
+
|
172
|
+
### Ruby ###
|
173
|
+
*.gem
|
174
|
+
/.config
|
175
|
+
/InstalledFiles
|
176
|
+
/pkg/
|
177
|
+
/spec/reports/
|
178
|
+
/spec/examples.txt
|
179
|
+
/test/tmp/
|
180
|
+
/test/version_tmp/
|
181
|
+
/tmp/
|
182
|
+
|
183
|
+
# Used by dotenv library to load environment variables.
|
184
|
+
# .env
|
185
|
+
|
186
|
+
# Ignore Byebug command history file.
|
187
|
+
|
188
|
+
## Specific to RubyMotion:
|
189
|
+
.dat*
|
190
|
+
.repl_history
|
191
|
+
build/
|
192
|
+
*.bridgesupport
|
193
|
+
build-iPhoneOS/
|
194
|
+
build-iPhoneSimulator/
|
195
|
+
|
196
|
+
## Specific to RubyMotion (use of CocoaPods):
|
197
|
+
#
|
198
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
199
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
200
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
201
|
+
# vendor/Pods/
|
202
|
+
|
203
|
+
## Documentation cache and generated files:
|
204
|
+
/.yardoc/
|
205
|
+
/_yardoc/
|
206
|
+
/doc/
|
207
|
+
/rdoc/
|
208
|
+
|
209
|
+
/.bundle/
|
210
|
+
/lib/bundler/man/
|
211
|
+
|
212
|
+
# for a library or gem, you might want to ignore these files since the code is
|
213
|
+
# intended to run in multiple environments; otherwise, check them in:
|
214
|
+
# Gemfile.lock
|
215
|
+
# .ruby-version
|
216
|
+
# .ruby-gemset
|
217
|
+
|
218
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
219
|
+
|
220
|
+
# End of https://www.gitignore.io/api/git,ruby,rails,jetbrains+all
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
volume_sweeper
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.1.4
|
data/.standard.yml
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# syntax=docker/dockerfile:1
|
2
|
+
|
3
|
+
FROM ruby:3.1.4 AS base
|
4
|
+
|
5
|
+
FROM base AS dependencies
|
6
|
+
RUN apt-get update -qq && apt-get install -y gcc cron
|
7
|
+
|
8
|
+
FROM dependencies as build
|
9
|
+
USER root
|
10
|
+
WORKDIR /app
|
11
|
+
COPY . .
|
12
|
+
RUN gem install bundler
|
13
|
+
RUN bundle install --without development
|
14
|
+
|
15
|
+
FROM build as runtime
|
16
|
+
|
17
|
+
COPY --from=build /app /app
|
18
|
+
|
19
|
+
EXPOSE 3000
|
20
|
+
CMD ["sh", "-c", "./bin/volume_sweeper --help"]
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Abdullah Barrak
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Volume Sweeper
|
2
|
+
[![CI (tests)](https://github.com/abarrak/volume_sweeper/actions/workflows/ci.yml/badge.svg)](https://github.com/abarrak/volume_sweeper/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/volume_sweeper.svg)](https://badge.fury.io/rb/volume_sweeper) [![Test Coverage](https://api.codeclimate.com/v1/badges/b9d24a336e67236937dd/test_coverage)](https://codeclimate.com/github/abarrak/volume_sweeper/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/b9d24a336e67236937dd/maintainability)](https://codeclimate.com/github/abarrak/volume_sweeper/maintainability) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
|
5
|
+
A tool to scan and clean cloud infrastruture for unattached block volumes without kubernetes clusters persistent volumes.
|
6
|
+
|
7
|
+
## Supported Clouds
|
8
|
+
|
9
|
+
- [x] OCI
|
10
|
+
- [ ] AWS.
|
11
|
+
- [ ] GCP.
|
12
|
+
|
13
|
+
## Supported Kubernetes
|
14
|
+
|
15
|
+
Any distributions + v1.19.
|
16
|
+
|
17
|
+
## Prerequisits
|
18
|
+
|
19
|
+
1. Kubernetes: a service account with read/update access to the cluster is required, scoped to `PV` resources.
|
20
|
+
2. Cloud: access is required for block volumes service (BV) with read and delete roles.
|
21
|
+
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
```bash
|
26
|
+
$ gem install volume_sweeper
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
To scan and generate a report:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
volume_sweeper --account-id <ID> --cloud aws|oci
|
35
|
+
```
|
36
|
+
|
37
|
+
To apply deletion for unattached block volumes:
|
38
|
+
|
39
|
+
```bash
|
40
|
+
volume_sweeper --mode delete
|
41
|
+
```
|
42
|
+
|
43
|
+
## Contributing
|
44
|
+
|
45
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/abarrak/volume_sweeper.
|
46
|
+
|
47
|
+
## License
|
48
|
+
|
49
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'cowsay'
|
4
|
+
require_relative 'utils/log'
|
5
|
+
|
6
|
+
module VolumeSweeper
|
7
|
+
class Cli
|
8
|
+
class << self
|
9
|
+
attr_reader :options
|
10
|
+
##
|
11
|
+
# A simple cli scriptlet to proccess command line arguments and pass them to
|
12
|
+
# the core component to run.
|
13
|
+
#
|
14
|
+
def run
|
15
|
+
print_banner
|
16
|
+
set_default_options
|
17
|
+
process_user_input
|
18
|
+
options
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def print_banner
|
24
|
+
puts Cowsay::say('Volume Sweeper 1.0', 'cow'), ""
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_user_input
|
28
|
+
OptionParser.new("== Usage: volume_sweeper.rb [options]") do |opt|
|
29
|
+
opt.on('-m', '--mode [MODE]', 'The run modes: either audit, or delete.') { |o| options.mode = o }
|
30
|
+
opt.on('-c', '--cloud [CLOUD]', 'Supported clouds: aws, oci.') { |o| options.cloud = o }
|
31
|
+
opt.on('-f', '--config-path [PATH]', 'The file location for cloud config file') { |o| options.config_path = o }
|
32
|
+
opt.on('-r', '--region [REGION]', 'The provider region of the account.') { |o| options.region = o }
|
33
|
+
opt.on('-a', '--account-id [Id]', 'The account or compartment Id.') { |o| options.account_id = o }
|
34
|
+
opt.on('-d', '--released-since [DAYS]', 'Volumes threshold duration') { |o| options.released_in_days = o }
|
35
|
+
opt.on('--kube-api-url [URL]', 'Kubernetes API URL') { |o| options.kube_api_url = o }
|
36
|
+
opt.on('--kube-api-ca-path [PATH]', 'Kubernetes API CA Cert Path') { |o| options.kube_api_ca_path = o }
|
37
|
+
opt.on('--kube-api-token [TOKEN]', 'Kubernetes API TOKEN (base64 formatted)') { |o| options.kube_api_token = o }
|
38
|
+
opt.on('--notification_subject [TEXT]', 'The message subject.') { |o| options.notification_subject = o }
|
39
|
+
opt.on('--smtp-host [HOST]', '') { |o| options.smtp_host = o }
|
40
|
+
opt.on('--smtp-port [PORT]', '') { |o| options.smtp_port = o }
|
41
|
+
opt.on('--smtp-username [USER]', '') { |o| options.smtp_username = o }
|
42
|
+
opt.on('--smtp-password [PASS]', '') { |o| options.smtp_password = o }
|
43
|
+
opt.on('--smtp-tls [ENABLE_TLS_FLAG]', '') { |o| options.smtp_tls = o }
|
44
|
+
opt.on('--smtp-sender [SENDER]', '') { |o| options.smtp_sender = o }
|
45
|
+
opt.on('--smtp-receiver [RECEIVER]', '') { |o| options.smtp_receiver = o }
|
46
|
+
opt.on('--ms-teams-webhook [URL]', '') { |o| options.ms_teams_webhook = o }
|
47
|
+
opt.on('-h', '--help') { |_| puts "", opt, "-" * 34; exit 0 }
|
48
|
+
end.parse!
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_default_options
|
52
|
+
@options = OpenStruct.new
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "active_support/core_ext/object/blank"
|
2
|
+
require_relative "utils/log"
|
3
|
+
|
4
|
+
module VolumeSweeper
|
5
|
+
module Comparer
|
6
|
+
LIST_SEP = "\n "
|
7
|
+
##
|
8
|
+
# Compare unattached block volumes against kubernetes persistent volumes
|
9
|
+
# in order to avoid any block volume that as volumeHandler reference,
|
10
|
+
# even if not bound to instance.
|
11
|
+
#
|
12
|
+
# === Docs:
|
13
|
+
#
|
14
|
+
# The algorithm goes as follows:
|
15
|
+
# ```
|
16
|
+
# FOR each_cluster IN oci:
|
17
|
+
# A] Fetch PVs (name, ocid)
|
18
|
+
# B] Fetch BLOCK VOL where attachment = nil
|
19
|
+
# C] Compare A ^ B to Extract Bx NOT IN Ax
|
20
|
+
# THEN:
|
21
|
+
# DEL [C] result
|
22
|
+
# End
|
23
|
+
# ```
|
24
|
+
#
|
25
|
+
def self.process block_volumes, persistent_volumes
|
26
|
+
unused_volumes = []
|
27
|
+
active_volumes = []
|
28
|
+
counters = { active: 0, unused: 0 }
|
29
|
+
|
30
|
+
return {} if block_volumes.blank? || persistent_volumes.blank?
|
31
|
+
|
32
|
+
block_volumes.each do |vol|
|
33
|
+
if persistent_volumes.any? { |p| p[:volumeHandle]&.strip == vol&.strip }
|
34
|
+
counters[:active] += 1
|
35
|
+
active_volumes << vol
|
36
|
+
else
|
37
|
+
counters[:unused] += 1
|
38
|
+
unused_volumes << vol
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
seperator = -> (str) { str.join(LIST_SEP).prepend LIST_SEP }
|
43
|
+
|
44
|
+
active_list = active_volumes.any? ? seperator.call(active_volumes) : 'None'
|
45
|
+
unused_list = unused_volumes.any? ? seperator.call(unused_volumes) : 'None'
|
46
|
+
|
47
|
+
Utils::Log.instance.msg "=> Found #{counters[:active]} still in use."
|
48
|
+
Utils::Log.instance.msg "=> Found #{counters[:unused]} unused and should be terminated."
|
49
|
+
Utils::Log.instance.msg "=> Details:"
|
50
|
+
Utils::Log.instance.msg "===> Active: ", active_list
|
51
|
+
Utils::Log.instance.msg "===> Unused: ", unused_list
|
52
|
+
|
53
|
+
{ active_ids: active_volumes, unused_ids: unused_volumes }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
require_relative 'cli'
|
3
|
+
require_relative 'comparer'
|
4
|
+
require_relative 'providers/aws'
|
5
|
+
require_relative 'providers/oci'
|
6
|
+
require_relative 'kube/client'
|
7
|
+
require_relative 'utils/log'
|
8
|
+
require_relative 'utils/notification'
|
9
|
+
require_relative 'utils/notification_formatter'
|
10
|
+
|
11
|
+
module VolumeSweeper
|
12
|
+
module Core
|
13
|
+
def self.process
|
14
|
+
# Process user input ..
|
15
|
+
opts = VolumeSweeper::Cli.run
|
16
|
+
cloud = opts.cloud
|
17
|
+
mode = opts.mode&.to_sym
|
18
|
+
options = {
|
19
|
+
config_path: opts.config_path,
|
20
|
+
account_id: opts.account_id,
|
21
|
+
region: opts.region,
|
22
|
+
mode: opts.mode,
|
23
|
+
kube_api_url: opts.kube_api_url,
|
24
|
+
kube_api_ca_path: opts.kube_api_ca_path,
|
25
|
+
kube_api_token: opts.kube_api_token,
|
26
|
+
notification_subject: opts.notification_subject,
|
27
|
+
smtp_host: opts.smtp_host,
|
28
|
+
smtp_port: opts.smtp_port,
|
29
|
+
smtp_username: opts.smtp_username,
|
30
|
+
smtp_password: opts.smtp_password,
|
31
|
+
smtp_tls: opts.smtp_tls,
|
32
|
+
smtp_sender: opts.smtp_sender,
|
33
|
+
smtp_receiver: opts.smtp_receiver,
|
34
|
+
ms_teams_webhook: opts.ms_teams_webhook
|
35
|
+
}
|
36
|
+
|
37
|
+
unless cloud.nil? || %w{oci aws}.include?(cloud)
|
38
|
+
Utils::Log.instance.msg "No could provider is chosen", level: :fatal
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
# Build and run provider checks ..
|
43
|
+
klass = cloud.capitalize
|
44
|
+
provider = "VolumeSweeper::Providers::#{klass}".constantize.new **options
|
45
|
+
active_count, block_vols = provider.scan_block_volumes
|
46
|
+
block_vols.map! { |v| v[:id] }
|
47
|
+
|
48
|
+
# Build and run kubernetes checks ..
|
49
|
+
kube_client = VolumeSweeper::Kube::Client.new **options
|
50
|
+
cluster_vols = kube_client.fetch_pesistent_volumes
|
51
|
+
|
52
|
+
# Prepare notification layer ..
|
53
|
+
notifier = VolumeSweeper::Utils::Notification.new **options
|
54
|
+
formatter = VolumeSweeper::Utils::NotificationFormatter.new provider.base_link, mode
|
55
|
+
|
56
|
+
# Run cross reference checks.
|
57
|
+
results = Comparer.process block_vols, cluster_vols
|
58
|
+
|
59
|
+
# Send notice messages.
|
60
|
+
message = formatter.formlate_meessage results, active_count: active_count
|
61
|
+
notifier.send_ms_teams_notice message
|
62
|
+
notifier.send_mail message if results[:unused_ids]&.any?
|
63
|
+
|
64
|
+
# Then, clean up any unattached block volume without PV bound to.
|
65
|
+
provider.delete_block_volumes results[:unused_ids]
|
66
|
+
|
67
|
+
# Wait for plaform logs aggregation.
|
68
|
+
sleep 30
|
69
|
+
|
70
|
+
VolumeSweeper::Utils::Log.instance.msg "Done !"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require "active_support/core_ext/object/blank"
|
2
|
+
require "base64"
|
3
|
+
require "kubeclient"
|
4
|
+
require_relative "../utils/log"
|
5
|
+
|
6
|
+
module VolumeSweeper
|
7
|
+
module Kube
|
8
|
+
##
|
9
|
+
#
|
10
|
+
# This class enables interation with the kube apis using 2 different modes.
|
11
|
+
#
|
12
|
+
# 1. In-cluster acccess.
|
13
|
+
# Assuming the code will run in the cluster it targets,
|
14
|
+
# So the main defaults that pods that mount from service account
|
15
|
+
# secret like `token` and `ca.cert` are used in this case.
|
16
|
+
#
|
17
|
+
# 2. External cluster access.
|
18
|
+
# Pass the environment variables below to access outside cluster
|
19
|
+
# * KUBE_API_URL
|
20
|
+
# * KUBE_API_TOKEN
|
21
|
+
# * KUBE_API_CA_PATH
|
22
|
+
# Or their kwargs counterpart to constructor.
|
23
|
+
# * :kube_api_url
|
24
|
+
# * :kube_api_token [base64 formatted]
|
25
|
+
# * :kube_api_ca_path
|
26
|
+
#
|
27
|
+
# The env vars take the highest precedence.
|
28
|
+
#
|
29
|
+
class Client
|
30
|
+
def initialize **kwargs
|
31
|
+
@run_mode = kwargs[:mode]&.to_sym || :audit
|
32
|
+
@api_url = kwargs[:kube_api_url]
|
33
|
+
@api_token = kwargs[:kube_api_token]
|
34
|
+
@api_ca_path = kwargs[:kube_api_ca_path]
|
35
|
+
|
36
|
+
@log = Utils::Log.instance
|
37
|
+
prepare_kube_client
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_pesistent_volumes
|
41
|
+
resources = []
|
42
|
+
make_api_call :get_persistent_volumes do |i|
|
43
|
+
resources << format_response_attrs(i)
|
44
|
+
end
|
45
|
+
@log.msg "kube: Collected #{resources.size} persisted volumes from the cluster."
|
46
|
+
|
47
|
+
resources
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_released_persistent_volumes age_in_days: 10
|
51
|
+
names = []
|
52
|
+
make_api_call :get_persistent_volumes do |i|
|
53
|
+
names << i[:metadat][:name] if i.dig(:status, :phase) == "Released"
|
54
|
+
end
|
55
|
+
@log.msg "kube: Collected #{resources.size} released persisted volumes."
|
56
|
+
|
57
|
+
return if names.blank? || @run_mode != :delete
|
58
|
+
|
59
|
+
# Do actual deletion for released persistent volumes.
|
60
|
+
# TODO: use last transistion timestamp if possible.
|
61
|
+
@log.msg "kube: Looping over #{names.size} persisted volumes to be deleted sequentially."
|
62
|
+
names.each do |pv|
|
63
|
+
@log.msg "kube: deleting #{names} persisted volume .."
|
64
|
+
@client.delete_pesistent_volume pv
|
65
|
+
sleep 2
|
66
|
+
end
|
67
|
+
@log.msg "kube: completed deletion of #{names.size} persisted volume."
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def prepare_kube_client
|
73
|
+
url = "#{build_cluster_url}/api"
|
74
|
+
@client ||= Kubeclient::Client.new url, "v1",
|
75
|
+
auth_options: build_auth_config,
|
76
|
+
ssl_options: build_ssl_config
|
77
|
+
@log.msg "Kube: running in :#{@run_mode} mode"
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_cluster_url
|
81
|
+
default_cluster_url = "https://kubernetes.default.svc"
|
82
|
+
ENV.fetch("KUBE_API_URL", @api_url || default_cluster_url)
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_ssl_config
|
86
|
+
default_ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
87
|
+
cluster_ca_path = ENV.fetch("KUBE_API_CA_PATH", @api_ca_path || default_ca_path)
|
88
|
+
|
89
|
+
Hash.new.tap do |h|
|
90
|
+
if File.exist? cluster_ca_path
|
91
|
+
h[:ca_file] = cluster_ca_path
|
92
|
+
else
|
93
|
+
h[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_auth_config
|
99
|
+
cluster_token = ENV["KUBE_API_SECRET"]
|
100
|
+
default_token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
101
|
+
Hash.new.tap do |h|
|
102
|
+
if cluster_token.present?
|
103
|
+
h[:bearer_token] = Base64.decode64 cluster_token
|
104
|
+
elsif @api_token.present?
|
105
|
+
h[:bearer_token] = Base64.decode64 @api_token
|
106
|
+
elsif File.exist? default_token_path
|
107
|
+
h[:bearer_token] = File.read default_token_path
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def make_api_call method, **opts
|
113
|
+
continue = nil
|
114
|
+
{ limit: 30 }.merge! opts
|
115
|
+
loop do
|
116
|
+
opts[:continue] = continue
|
117
|
+
output = @client.send method, **opts
|
118
|
+
continue = output.continue
|
119
|
+
output.map(&:to_h).collect do |i|
|
120
|
+
yield i
|
121
|
+
end
|
122
|
+
break if output.last?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def format_response_attrs item
|
127
|
+
{
|
128
|
+
name: item.dig(:metadat, :name),
|
129
|
+
status: item.dig(:status, :phase),
|
130
|
+
volumeHandle: item.dig(:spec, :csi, :volumeHandle),
|
131
|
+
pvc: item.dig(:spec, :claimRef, :name),
|
132
|
+
namespace: item.dig(:spec, :claimRef, :namespace)
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|