equilibrium 0.1.0
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 +7 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +40 -0
- data/LICENSE +21 -0
- data/README.md +312 -0
- data/Rakefile +9 -0
- data/equilibrium +9 -0
- data/exe/equilibrium +6 -0
- data/lib/equilibrium/analyzer.rb +109 -0
- data/lib/equilibrium/catalog_builder.rb +45 -0
- data/lib/equilibrium/cli.rb +210 -0
- data/lib/equilibrium/registry_client.rb +101 -0
- data/lib/equilibrium/schema_validator.rb +29 -0
- data/lib/equilibrium/schemas/analyzer_output.rb +101 -0
- data/lib/equilibrium/schemas/catalog.rb +74 -0
- data/lib/equilibrium/schemas/expected_actual.rb +74 -0
- data/lib/equilibrium/schemas/registry_api.rb +63 -0
- data/lib/equilibrium/semantic_version.rb +24 -0
- data/lib/equilibrium/summary_formatter.rb +116 -0
- data/lib/equilibrium/tag_processor.rb +84 -0
- data/lib/equilibrium/version.rb +5 -0
- data/lib/equilibrium.rb +13 -0
- data/tmp/.gitkeep +2 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5420df18928550fa411f853968f4b5ac331fbcd3bcc682dce61370c330dd69bc
|
4
|
+
data.tar.gz: 062322ae383ea3aba97d0d60980f8530808e7877e5bb5a6ce311435ec6a86814
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3900bcb75092a001f250dbe206d4085419435e7d2b370c3a68e57e82619e1a7b014dd32a71510f887e060244c122336f70f946bd836e1c2ef956d5500416d93c
|
7
|
+
data.tar.gz: 2678994b5a362aa3e282618dd49f401aa63d50905f13b0fc74323d0747dc73d076aa872c915843e8d4002a9f1042992e33b5251a5f10cbeb9405d8bb2d29454d
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--format documentation
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.8
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.1.0] - 2025-08-05
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Initial release of Equilibrium container image validation tool
|
12
|
+
- Core CLI with Unix-style command interface (`expected`, `actual`, `analyze`, `catalog`)
|
13
|
+
- Support for Google Container Registry (GCR) public repositories
|
14
|
+
- Semantic version tag processing and mutable tag computation
|
15
|
+
- JSON schema validation for all command outputs
|
16
|
+
- Comprehensive analysis and remediation planning functionality
|
17
|
+
- Ruby gem packaging with executable installation
|
18
|
+
- Thor-based command-line interface with table formatting
|
19
|
+
- Dual format output support (summary and JSON) for analysis
|
20
|
+
- Catalog generation with canonical version mapping
|
21
|
+
- Comprehensive test suite with RSpec
|
22
|
+
- CI/CD pipeline with GitHub Actions
|
23
|
+
- Trusted publishing support for RubyGems
|
24
|
+
- Standard rake task integration
|
25
|
+
|
26
|
+
### Features
|
27
|
+
- **Tag Validation**: Validates equilibrium between mutable tags and semantic version tags
|
28
|
+
- **Registry Client**: Pure Ruby HTTP client for container registry API access
|
29
|
+
- **Tag Processing**: Computes expected mutable tags from semantic versions (latest, major, minor)
|
30
|
+
- **Analysis Engine**: Compares expected vs actual tags with detailed remediation planning
|
31
|
+
- **Schema Validation**: Embedded JSON schemas for all data formats
|
32
|
+
- **Unix Philosophy**: Composable commands that work well in pipelines
|
33
|
+
- **Output Formats**: Multiple output formats including JSON, summary tables, and catalog
|
34
|
+
|
35
|
+
### Documentation
|
36
|
+
- Comprehensive README with usage examples and API documentation
|
37
|
+
- Detailed architecture overview and data flow diagrams
|
38
|
+
- Complete command reference and examples
|
39
|
+
|
40
|
+
[0.1.0]: https://github.com/DataDog/equilibrium/releases/tag/v0.1.0
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Tony Hsu
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,312 @@
|
|
1
|
+
# Equilibrium
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/equilibrium)
|
4
|
+
[](https://github.com/TonyCTHsu/equilibrium/actions)
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
6
|
+
|
7
|
+
A container image tool that validates equilibrium between mutable tags and semantic version tags.
|
8
|
+
|
9
|
+
## Table of Contents
|
10
|
+
|
11
|
+
- [The Problem](#the-problem)
|
12
|
+
- [How Equilibrium Solves It](#how-equilibrium-solves-it)
|
13
|
+
- [Tag Conversion Logic](#tag-conversion-logic)
|
14
|
+
- [Installation](#installation)
|
15
|
+
- [Quick Start](#quick-start)
|
16
|
+
- [Output Formats & Schemas](#output-formats--schemas)
|
17
|
+
- [Constraints](#constraints)
|
18
|
+
- [Examples](#examples)
|
19
|
+
- [License](#license)
|
20
|
+
|
21
|
+
## The Problem
|
22
|
+
|
23
|
+
Container registries create a web of mutable tags that should point to specific semantic versions, but these relationships can break over time:
|
24
|
+
|
25
|
+
**The Challenge:**
|
26
|
+
- When you publish `myapp:1.2.3`, registries should automatically create `myapp:latest`, `myapp:1`, `myapp:1.2`
|
27
|
+
- But what happens when you later publish `myapp:1.2.4` or `myapp:1.3.0`?
|
28
|
+
- Do all the mutable tags get updated correctly?
|
29
|
+
- How do you verify the entire tag ecosystem is in equilibrium?
|
30
|
+
|
31
|
+
**Real-World Impact:**
|
32
|
+
- `latest` might point to an outdated version
|
33
|
+
- Major version tags like `1` might miss recent patches
|
34
|
+
- Minor version tags like `1.2` might not reflect the latest patch
|
35
|
+
- Users pulling `myapp:1` expect the latest `1.x.x`, but get an old version
|
36
|
+
|
37
|
+
## How Equilibrium Solves It
|
38
|
+
|
39
|
+
Equilibrium validates your container registry's tag ecosystem through a clear data flow:
|
40
|
+
|
41
|
+
```mermaid
|
42
|
+
flowchart LR
|
43
|
+
A[Registry Query] --> B[All Tags<br/>1.0.0<br/>1.1.0<br/>latest<br/>1<br/>1.1<br/>dev]
|
44
|
+
|
45
|
+
B --> C[Filter<br/>Semantic<br/>Versions]
|
46
|
+
B --> D[Filter<br/>Mutable<br/>Tags]
|
47
|
+
|
48
|
+
C --> E[Semantic Versions<br/>1.0.0<br/>1.1.0]
|
49
|
+
D --> F[Actual Mutable<br/>latest→1.0<br/>1→1.0.0<br/>1.1→1.1.0]
|
50
|
+
|
51
|
+
E --> G[Compute<br/>Expected<br/>Mutable Tags]
|
52
|
+
G --> H[Expected Mutable<br/>latest→1.1<br/>1→1.1.0<br/>1.1→1.1.0]
|
53
|
+
|
54
|
+
H --> I[Compare<br/>Expected vs<br/>Actual]
|
55
|
+
F --> I
|
56
|
+
|
57
|
+
I --> J[Analysis &<br/>Remediation<br/>Update: latest]
|
58
|
+
|
59
|
+
classDef source fill:#e1d5e7
|
60
|
+
classDef process fill:#fff2cc
|
61
|
+
classDef expected fill:#d5e8d4
|
62
|
+
classDef actual fill:#f8cecc
|
63
|
+
classDef result fill:#ffcccc
|
64
|
+
|
65
|
+
class A source
|
66
|
+
class C,D,G,I process
|
67
|
+
class E,H expected
|
68
|
+
class F actual
|
69
|
+
class J result
|
70
|
+
```
|
71
|
+
|
72
|
+
**Process Steps:**
|
73
|
+
1. **Fetch All Tags**: Query registry for complete tag list
|
74
|
+
2. **Filter Semantic**: Extract only valid semantic version tags (MAJOR.MINOR.PATCH)
|
75
|
+
3. **Compute Expected**: Generate mutable tags based on semantic versions
|
76
|
+
4. **Fetch Actual**: Query registry for current mutable tag state
|
77
|
+
5. **Compare & Analyze**: Identify mismatches and generate remediation plan
|
78
|
+
|
79
|
+
## Installation
|
80
|
+
|
81
|
+
```bash
|
82
|
+
gem install equilibrium
|
83
|
+
```
|
84
|
+
|
85
|
+
*For other installation methods, see `equilibrium help`*
|
86
|
+
|
87
|
+
## Quick Start
|
88
|
+
|
89
|
+
```bash
|
90
|
+
REPO="gcr.io/datadoghq/apm-inject"
|
91
|
+
# 1. Check what mutable tags should exist
|
92
|
+
equilibrium expected "$REPO"
|
93
|
+
|
94
|
+
# 2. Check what mutable tags actually exist
|
95
|
+
equilibrium actual "$REPO"
|
96
|
+
|
97
|
+
# 3. Compare and get remediation plan
|
98
|
+
equilibrium expected "$REPO" --format json > expected.json
|
99
|
+
equilibrium actual "$REPO" --format json > actual.json
|
100
|
+
equilibrium analyze --expected expected.json --actual actual.json
|
101
|
+
```
|
102
|
+
|
103
|
+
*For detailed command options, run `equilibrium help [command]`*
|
104
|
+
|
105
|
+
## Tag Conversion Logic
|
106
|
+
|
107
|
+
Equilibrium transforms semantic version tags into their expected mutable tag ecosystem:
|
108
|
+
|
109
|
+
### Core Principle
|
110
|
+
For any semantic version `MAJOR.MINOR.PATCH`, create mutable tags for each component level, pointing to the **highest available version** at that level.
|
111
|
+
|
112
|
+
### Detailed Examples
|
113
|
+
|
114
|
+
**Scenario 1: Single Version**
|
115
|
+
```
|
116
|
+
Semantic versions: ["1.0.0"]
|
117
|
+
|
118
|
+
Expected mutable tags:
|
119
|
+
├── latest → 1.0.0 (highest overall)
|
120
|
+
├── 1 → 1.0.0 (highest major 1)
|
121
|
+
└── 1.0 → 1.0.0 (highest minor 1.0)
|
122
|
+
```
|
123
|
+
|
124
|
+
**Scenario 2: Multiple Patch Versions**
|
125
|
+
```
|
126
|
+
Semantic versions: ["1.0.0", "1.0.1", "1.0.2"]
|
127
|
+
|
128
|
+
Expected mutable tags:
|
129
|
+
├── latest → 1.0.2 (highest overall)
|
130
|
+
├── 1 → 1.0.2 (highest major 1)
|
131
|
+
└── 1.0 → 1.0.2 (highest minor 1.0)
|
132
|
+
```
|
133
|
+
|
134
|
+
**Scenario 3: Multiple Minor Versions**
|
135
|
+
```
|
136
|
+
Semantic versions: ["1.0.0", "1.0.1", "1.1.0", "1.1.3"]
|
137
|
+
|
138
|
+
Expected mutable tags:
|
139
|
+
├── latest → 1.1.3 (highest overall)
|
140
|
+
├── 1 → 1.1.3 (highest major 1)
|
141
|
+
├── 1.0 → 1.0.1 (highest minor 1.0)
|
142
|
+
└── 1.1 → 1.1.3 (highest minor 1.1)
|
143
|
+
```
|
144
|
+
|
145
|
+
**Scenario 4: Multiple Major Versions**
|
146
|
+
```
|
147
|
+
Semantic versions: ["0.9.0", "1.0.0", "1.2.3", "2.0.0", "2.1.0"]
|
148
|
+
|
149
|
+
Expected mutable tags:
|
150
|
+
├── latest → 2.1.0 (highest overall)
|
151
|
+
├── 0 → 0.9.0 (highest major 0)
|
152
|
+
├── 0.9 → 0.9.0 (highest minor 0.9)
|
153
|
+
├── 1 → 1.2.3 (highest major 1)
|
154
|
+
├── 1.0 → 1.0.0 (highest minor 1.0)
|
155
|
+
├── 1.2 → 1.2.3 (highest minor 1.2)
|
156
|
+
├── 2 → 2.1.0 (highest major 2)
|
157
|
+
├── 2.0 → 2.0.0 (highest minor 2.0)
|
158
|
+
└── 2.1 → 2.1.0 (highest minor 2.1)
|
159
|
+
```
|
160
|
+
|
161
|
+
**Scenario 5: Pre-release and Non-semantic Tags (Filtered Out)**
|
162
|
+
```
|
163
|
+
All tags: ["1.0.0", "1.1.0-beta", "1.1.0-rc1", "1.1.0", "latest", "dev"]
|
164
|
+
Semantic versions: ["1.0.0", "1.1.0"] # Pre-releases and existing mutable tags filtered
|
165
|
+
|
166
|
+
Expected mutable tags:
|
167
|
+
├── latest → 1.1.0 (highest overall)
|
168
|
+
├── 1 → 1.1.0 (highest major 1)
|
169
|
+
├── 1.0 → 1.0.0 (highest minor 1.0)
|
170
|
+
└── 1.1 → 1.1.0 (highest minor 1.1)
|
171
|
+
```
|
172
|
+
|
173
|
+
## Output Formats & Schemas
|
174
|
+
|
175
|
+
### JSON Format
|
176
|
+
All commands output structured JSON following validated schemas (see [schemas](lib/equilibrium/schemas/)):
|
177
|
+
|
178
|
+
**Expected/Actual Commands** ([schema](lib/equilibrium/schemas/expected_actual.rb)):
|
179
|
+
```json
|
180
|
+
{
|
181
|
+
"repository_url": "gcr.io/project/image",
|
182
|
+
"repository_name": "image",
|
183
|
+
"digests": {
|
184
|
+
"latest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
185
|
+
"1": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
186
|
+
"1.2": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c"
|
187
|
+
},
|
188
|
+
"canonical_versions": {
|
189
|
+
"latest": "1.2.3",
|
190
|
+
"1": "1.2.3",
|
191
|
+
"1.2": "1.2.3"
|
192
|
+
}
|
193
|
+
}
|
194
|
+
```
|
195
|
+
|
196
|
+
**Analyze Command** ([schema](lib/equilibrium/schemas/analyzer_output.rb)):
|
197
|
+
```json
|
198
|
+
{
|
199
|
+
"repository_url": "gcr.io/project/image",
|
200
|
+
"expected_count": 3,
|
201
|
+
"actual_count": 2,
|
202
|
+
"missing_tags": {
|
203
|
+
"1.2": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890"
|
204
|
+
},
|
205
|
+
"unexpected_tags": {},
|
206
|
+
"mismatched_tags": {
|
207
|
+
"latest": {
|
208
|
+
"expected": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
|
209
|
+
"actual": "sha256:old123ef456789012345678901234567890123456789012345678901234567890"
|
210
|
+
}
|
211
|
+
},
|
212
|
+
"status": "missing_tags",
|
213
|
+
"remediation_plan": [
|
214
|
+
{
|
215
|
+
"action": "create_tag",
|
216
|
+
"tag": "1.2",
|
217
|
+
"digest": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890",
|
218
|
+
"command": "gcloud container images add-tag gcr.io/project/image@sha256:abc123... gcr.io/project/image:1.2"
|
219
|
+
},
|
220
|
+
{
|
221
|
+
"action": "update_tag",
|
222
|
+
"tag": "latest",
|
223
|
+
"old_digest": "sha256:old123ef456789012345678901234567890123456789012345678901234567890",
|
224
|
+
"new_digest": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
|
225
|
+
"command": "gcloud container images add-tag gcr.io/project/image@sha256:def456... gcr.io/project/image:latest"
|
226
|
+
}
|
227
|
+
]
|
228
|
+
}
|
229
|
+
```
|
230
|
+
|
231
|
+
**Catalog Command** ([schema](lib/equilibrium/schemas/catalog.rb)):
|
232
|
+
```json
|
233
|
+
{
|
234
|
+
"images": [
|
235
|
+
{
|
236
|
+
"name": "image",
|
237
|
+
"tag": "latest",
|
238
|
+
"digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
239
|
+
"canonical_version": "1.2.3"
|
240
|
+
},
|
241
|
+
{
|
242
|
+
"name": "image",
|
243
|
+
"tag": "1",
|
244
|
+
"digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
245
|
+
"canonical_version": "1.2.3"
|
246
|
+
},
|
247
|
+
{
|
248
|
+
"name": "image",
|
249
|
+
"tag": "1.2",
|
250
|
+
"digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
251
|
+
"canonical_version": "1.2.3"
|
252
|
+
}
|
253
|
+
]
|
254
|
+
}
|
255
|
+
```
|
256
|
+
|
257
|
+
### Summary Format
|
258
|
+
Human-readable table format for quick visual inspection.
|
259
|
+
|
260
|
+
## Constraints
|
261
|
+
|
262
|
+
- **Registry Support**: Public Google Container Registry (GCR) only
|
263
|
+
- **Tag Format**: Only processes semantic version tags (MAJOR.MINOR.PATCH)
|
264
|
+
- **URL Format**: Requires full repository URLs: `[REGISTRY_HOST]/[NAMESPACE]/[REPOSITORY]`
|
265
|
+
|
266
|
+
## Examples
|
267
|
+
|
268
|
+
### Example 1: Perfect Equilibrium
|
269
|
+
```bash
|
270
|
+
$ equilibrium expected gcr.io/google-containers/pause
|
271
|
+
# Shows: latest→3.9, 3→3.9, 3.9→3.9
|
272
|
+
|
273
|
+
$ equilibrium actual gcr.io/google-containers/pause
|
274
|
+
# Shows: latest→3.9, 3→3.9, 3.9→3.9
|
275
|
+
|
276
|
+
$ equilibrium analyze --expected expected.json --actual actual.json
|
277
|
+
# Status: ✅ in_equilibrium
|
278
|
+
```
|
279
|
+
|
280
|
+
### Example 2: Out of Equilibrium
|
281
|
+
```bash
|
282
|
+
$ equilibrium expected gcr.io/project/myapp
|
283
|
+
# Expected: latest→2.1.0, 1→1.5.3, 2→2.1.0, 2.1→2.1.0
|
284
|
+
|
285
|
+
$ equilibrium actual gcr.io/project/myapp
|
286
|
+
# Actual: latest→1.5.3, 1→1.5.3 (missing: 2, 2.1)
|
287
|
+
|
288
|
+
$ equilibrium analyze --expected expected.json --actual actual.json
|
289
|
+
# Status: ❌ out_of_equilibrium
|
290
|
+
# Remediation: Create tags: 2→2.1.0, 2.1→2.1.0; Update: latest→2.1.0
|
291
|
+
```
|
292
|
+
|
293
|
+
### Example 3: Automation Pipeline
|
294
|
+
```bash
|
295
|
+
#!/bin/bash
|
296
|
+
REPO="gcr.io/project/image"
|
297
|
+
|
298
|
+
# Daily equilibrium check
|
299
|
+
equilibrium expected "$REPO" > expected.json
|
300
|
+
equilibrium actual "$REPO" > actual.json
|
301
|
+
equilibrium analyze --expected expected.json --actual actual.json --format json > report.json
|
302
|
+
|
303
|
+
# Alert if out of equilibrium
|
304
|
+
if grep -q '"status": "out_of_equilibrium"' report.json; then
|
305
|
+
echo "⚠️ Repository $REPO is out of equilibrium!"
|
306
|
+
equilibrium analyze --expected expected.json --actual actual.json
|
307
|
+
fi
|
308
|
+
```
|
309
|
+
|
310
|
+
## License
|
311
|
+
|
312
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
data/Rakefile
ADDED
data/equilibrium
ADDED
data/exe/equilibrium
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Equilibrium
|
6
|
+
class Analyzer
|
7
|
+
def initialize
|
8
|
+
end
|
9
|
+
|
10
|
+
# Analyzes validated expected/actual data in schema format
|
11
|
+
def analyze(expected_data, actual_data)
|
12
|
+
# Extract digests from validated schema format
|
13
|
+
expected_tags = expected_data["digests"]
|
14
|
+
actual_tags = actual_data["digests"]
|
15
|
+
|
16
|
+
# Extract and validate repository URLs match
|
17
|
+
expected_url = expected_data["repository_url"]
|
18
|
+
actual_url = actual_data["repository_url"]
|
19
|
+
|
20
|
+
if expected_url != actual_url
|
21
|
+
raise ArgumentError, "Repository URLs do not match: expected '#{expected_url}', actual '#{actual_url}'"
|
22
|
+
end
|
23
|
+
|
24
|
+
final_repository_url = expected_url
|
25
|
+
|
26
|
+
analysis = {
|
27
|
+
repository_url: final_repository_url,
|
28
|
+
expected_count: expected_tags.size,
|
29
|
+
actual_count: actual_tags.size,
|
30
|
+
missing_tags: find_missing_tags(expected_tags, actual_tags),
|
31
|
+
unexpected_tags: find_unexpected_tags(expected_tags, actual_tags),
|
32
|
+
mismatched_tags: find_mismatched_tags(expected_tags, actual_tags),
|
33
|
+
status: determine_status(expected_tags, actual_tags)
|
34
|
+
}
|
35
|
+
|
36
|
+
# Add remediation plan for JSON format
|
37
|
+
analysis[:remediation_plan] = generate_remediation_plan(analysis, final_repository_url)
|
38
|
+
analysis
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def generate_remediation_plan(analysis, repository_url)
|
44
|
+
plan = []
|
45
|
+
|
46
|
+
analysis[:missing_tags].each do |tag, digest|
|
47
|
+
plan << {
|
48
|
+
action: "create_tag",
|
49
|
+
tag: tag,
|
50
|
+
digest: digest,
|
51
|
+
command: "gcloud container images add-tag #{repository_url}@#{digest} #{repository_url}:#{tag}"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
analysis[:mismatched_tags].each do |tag, data|
|
56
|
+
plan << {
|
57
|
+
action: "update_tag",
|
58
|
+
tag: tag,
|
59
|
+
old_digest: data[:actual],
|
60
|
+
new_digest: data[:expected],
|
61
|
+
command: "gcloud container images add-tag #{repository_url}@#{data[:expected]} #{repository_url}:#{tag}"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
analysis[:unexpected_tags].each do |tag, digest|
|
66
|
+
plan << {
|
67
|
+
action: "remove_tag",
|
68
|
+
tag: tag,
|
69
|
+
digest: digest,
|
70
|
+
command: "gcloud container images untag #{repository_url}:#{tag}"
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
plan
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def find_missing_tags(expected, actual)
|
80
|
+
expected.reject { |tag, digest| actual.key?(tag) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_unexpected_tags(expected, actual)
|
84
|
+
actual.reject { |tag, _| expected.key?(tag) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_mismatched_tags(expected, actual)
|
88
|
+
mismatched = {}
|
89
|
+
expected.each do |tag, expected_digest|
|
90
|
+
if actual.key?(tag) && actual[tag] != expected_digest
|
91
|
+
mismatched[tag] = {
|
92
|
+
expected: expected_digest,
|
93
|
+
actual: actual[tag]
|
94
|
+
}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
mismatched
|
98
|
+
end
|
99
|
+
|
100
|
+
def determine_status(expected, actual)
|
101
|
+
return "perfect" if expected == actual
|
102
|
+
return "mismatched" if find_mismatched_tags(expected, actual).any?
|
103
|
+
return "missing_tags" if find_missing_tags(expected, actual).any?
|
104
|
+
return "extra_tags" if find_unexpected_tags(expected, actual).any?
|
105
|
+
|
106
|
+
"perfect" # Default to perfect if no differences found
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "json_schemer"
|
5
|
+
require_relative "schemas/catalog"
|
6
|
+
|
7
|
+
module Equilibrium
|
8
|
+
class CatalogBuilder
|
9
|
+
class Error < StandardError; end
|
10
|
+
|
11
|
+
def build_catalog(data)
|
12
|
+
# Extract repository name, digests, and canonical versions from the validated data structure
|
13
|
+
repository_name = data["repository_name"]
|
14
|
+
digests = data["digests"]
|
15
|
+
canonical_versions = data["canonical_versions"]
|
16
|
+
|
17
|
+
images = digests.map do |tag, digest|
|
18
|
+
{
|
19
|
+
"name" => repository_name,
|
20
|
+
"tag" => tag,
|
21
|
+
"digest" => digest,
|
22
|
+
"canonical_version" => canonical_versions[tag]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
catalog = {
|
27
|
+
"images" => images
|
28
|
+
}
|
29
|
+
|
30
|
+
validate_catalog(catalog)
|
31
|
+
catalog
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_catalog(catalog)
|
37
|
+
schemer = JSONSchemer.schema(Equilibrium::Schemas::CATALOG)
|
38
|
+
errors = schemer.validate(catalog).to_a
|
39
|
+
|
40
|
+
unless errors.empty?
|
41
|
+
raise Error, "Catalog validation failed: #{errors.map(&:to_s).join(", ")}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|