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
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
module Equilibrium
|
6
|
+
class SummaryFormatter
|
7
|
+
include Thor::Shell
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# Initialize Thor::Shell methods with empty arguments
|
11
|
+
self.shell = Thor::Shell::Basic.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def print_analysis_summary(analysis)
|
15
|
+
say "Repository URL: #{analysis[:repository_url]}"
|
16
|
+
say ""
|
17
|
+
|
18
|
+
# Status overview table
|
19
|
+
status_color = (analysis[:status] == "perfect") ? :green : :yellow
|
20
|
+
status_symbol = (analysis[:status] == "perfect") ? "✓" : "⚠"
|
21
|
+
|
22
|
+
overview_data = [
|
23
|
+
["Metric", "Count"],
|
24
|
+
["Expected tags", analysis[:expected_count].to_s],
|
25
|
+
["Actual tags", analysis[:actual_count].to_s],
|
26
|
+
["Missing tags", analysis[:missing_tags].size.to_s],
|
27
|
+
["Mismatched tags", analysis[:mismatched_tags].size.to_s],
|
28
|
+
["Unexpected tags", analysis[:unexpected_tags].size.to_s]
|
29
|
+
]
|
30
|
+
|
31
|
+
say "Analysis Overview:"
|
32
|
+
print_table(overview_data, borders: true)
|
33
|
+
say ""
|
34
|
+
|
35
|
+
say "#{status_symbol} Status: #{analysis[:status].upcase.tr("_", " ")}", status_color
|
36
|
+
say ""
|
37
|
+
|
38
|
+
# Show specific issue sections
|
39
|
+
has_issues = false
|
40
|
+
|
41
|
+
# Missing tags section
|
42
|
+
if analysis[:missing_tags].any?
|
43
|
+
has_issues = true
|
44
|
+
say "Missing Tags (should be created):"
|
45
|
+
missing_table = [["Tag", "Should Point To"]]
|
46
|
+
analysis[:missing_tags].each do |tag, digest|
|
47
|
+
short_digest = digest ? digest.split(":").last[0..11] : "unknown"
|
48
|
+
missing_table << [tag, short_digest]
|
49
|
+
end
|
50
|
+
print_table(missing_table, borders: true)
|
51
|
+
say ""
|
52
|
+
end
|
53
|
+
|
54
|
+
# Mismatched tags section
|
55
|
+
if analysis[:mismatched_tags].any?
|
56
|
+
has_issues = true
|
57
|
+
say "Mismatched Tags (pointing to wrong version):"
|
58
|
+
mismatched_table = [["Tag", "Expected", "Actual"]]
|
59
|
+
analysis[:mismatched_tags].each do |tag, details|
|
60
|
+
if details.is_a?(Hash)
|
61
|
+
expected = details[:expected] ? details[:expected].split(":").last[0..11] : "unknown"
|
62
|
+
actual = details[:actual] ? details[:actual].split(":").last[0..11] : "unknown"
|
63
|
+
else
|
64
|
+
expected = details ? details.split(":").last[0..11] : "unknown"
|
65
|
+
actual = "unknown"
|
66
|
+
end
|
67
|
+
mismatched_table << [tag, expected, actual]
|
68
|
+
end
|
69
|
+
print_table(mismatched_table, borders: true)
|
70
|
+
say ""
|
71
|
+
end
|
72
|
+
|
73
|
+
# Unexpected tags section
|
74
|
+
if analysis[:unexpected_tags].any?
|
75
|
+
has_issues = true
|
76
|
+
say "Unexpected Tags (should be removed):"
|
77
|
+
unexpected_table = [["Tag", "Currently Points To"]]
|
78
|
+
analysis[:unexpected_tags].each do |tag, digest|
|
79
|
+
short_digest = digest ? digest.split(":").last[0..11] : "unknown"
|
80
|
+
unexpected_table << [tag, short_digest]
|
81
|
+
end
|
82
|
+
print_table(unexpected_table, borders: true)
|
83
|
+
say ""
|
84
|
+
end
|
85
|
+
|
86
|
+
if has_issues
|
87
|
+
say "To see detailed remediation commands, use:"
|
88
|
+
say " equilibrium analyze --expected expected.json --actual actual.json --format=json | jq '.remediation_plan'"
|
89
|
+
else
|
90
|
+
say "✓ Registry is in perfect equilibrium!", :green
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def print_tags_summary(output, type)
|
95
|
+
say "Repository: #{output["repository_name"]}"
|
96
|
+
say "URL: #{output["repository_url"]}"
|
97
|
+
say ""
|
98
|
+
|
99
|
+
mutable_tags = output["digests"]
|
100
|
+
canonical_versions = output["canonical_versions"]
|
101
|
+
|
102
|
+
say "#{type.capitalize} mutable tags (#{mutable_tags.size}):"
|
103
|
+
say ""
|
104
|
+
|
105
|
+
# Create table data: [["Tag", "Version", "Digest"]]
|
106
|
+
table_data = [["Tag", "Version", "Digest"]]
|
107
|
+
mutable_tags.keys.sort.each do |tag|
|
108
|
+
canonical_version = canonical_versions[tag]
|
109
|
+
digest = mutable_tags[tag]
|
110
|
+
table_data << [tag, canonical_version, digest]
|
111
|
+
end
|
112
|
+
|
113
|
+
print_table(table_data, borders: true)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Equilibrium
|
6
|
+
class TagProcessor
|
7
|
+
def self.compute_virtual_tags(semantic_tags)
|
8
|
+
new.compute_virtual_tags(semantic_tags)
|
9
|
+
end
|
10
|
+
|
11
|
+
def compute_virtual_tags(semantic_tags)
|
12
|
+
return {"digests" => {}, "canonical_versions" => {}} if semantic_tags.empty?
|
13
|
+
|
14
|
+
latest_version = nil
|
15
|
+
major_versions = {}
|
16
|
+
minor_versions = {}
|
17
|
+
|
18
|
+
# Single pass to find all maximums
|
19
|
+
semantic_tags.keys.each do |version_str|
|
20
|
+
version = Gem::Version.new(version_str)
|
21
|
+
|
22
|
+
# Track overall latest
|
23
|
+
latest_version = version if !latest_version || version > latest_version
|
24
|
+
|
25
|
+
# Track latest for each major
|
26
|
+
major = version.segments[0]
|
27
|
+
major_versions[major] = version if !major_versions[major] || version > major_versions[major]
|
28
|
+
|
29
|
+
# Track latest for each minor
|
30
|
+
major_minor = "#{version.segments[0]}.#{version.segments[1]}"
|
31
|
+
minor_versions[major_minor] = version if !minor_versions[major_minor] || version > minor_versions[major_minor]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Build result with both digest and canonical mappings
|
35
|
+
digests = {}
|
36
|
+
canonical_versions = {}
|
37
|
+
|
38
|
+
if latest_version
|
39
|
+
digests["latest"] = semantic_tags[latest_version.to_s]
|
40
|
+
canonical_versions["latest"] = latest_version.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
major_versions.each do |_, v|
|
44
|
+
tag = v.segments[0].to_s
|
45
|
+
digests[tag] = semantic_tags[v.to_s]
|
46
|
+
canonical_versions[tag] = v.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
minor_versions.each do |_, v|
|
50
|
+
tag = "#{v.segments[0]}.#{v.segments[1]}"
|
51
|
+
digests[tag] = semantic_tags[v.to_s]
|
52
|
+
canonical_versions[tag] = v.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
{"digests" => digests, "canonical_versions" => canonical_versions}
|
56
|
+
end
|
57
|
+
|
58
|
+
def filter_semantic_tags(all_tags)
|
59
|
+
# Filter semantic tags (canonical_tags.json): exact major.minor.patch format
|
60
|
+
filtered = all_tags.select { |tag, _| semantic_version?(tag) }
|
61
|
+
# Sort by key in reverse order (matching original jq: sort_by(.key) | reverse)
|
62
|
+
filtered.sort_by { |tag, _| tag }.reverse.to_h
|
63
|
+
end
|
64
|
+
|
65
|
+
def filter_mutable_tags(all_tags)
|
66
|
+
# Filter mutable tags (actual_tags.json): latest, digits, or major.minor format
|
67
|
+
filtered = all_tags.select { |tag, _| mutable_tag?(tag) }
|
68
|
+
# Sort by key in reverse order (matching original jq: sort_by(.key) | reverse)
|
69
|
+
filtered.sort_by { |tag, _| tag }.reverse.to_h
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def semantic_version?(tag)
|
75
|
+
SemanticVersion.valid?(tag)
|
76
|
+
end
|
77
|
+
|
78
|
+
def mutable_tag?(tag)
|
79
|
+
tag.match?(/^latest$/) ||
|
80
|
+
tag.match?(/^[0-9]+$/) ||
|
81
|
+
tag.match?(/^[0-9]+\.[0-9]+$/)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/equilibrium.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "equilibrium/version"
|
4
|
+
require_relative "equilibrium/semantic_version"
|
5
|
+
require_relative "equilibrium/registry_client"
|
6
|
+
require_relative "equilibrium/tag_processor"
|
7
|
+
require_relative "equilibrium/catalog_builder"
|
8
|
+
require_relative "equilibrium/analyzer"
|
9
|
+
require_relative "equilibrium/schemas/expected_actual"
|
10
|
+
|
11
|
+
module Equilibrium
|
12
|
+
class Error < StandardError; end
|
13
|
+
end
|
data/tmp/.gitkeep
ADDED
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: equilibrium
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tony Hsu
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json_schemer
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
description: Validates equilibrium between mutable tags and semantic version tags
|
56
|
+
in container registries
|
57
|
+
email:
|
58
|
+
- tonyc.t.hsu@gmail.com
|
59
|
+
executables:
|
60
|
+
- equilibrium
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- ".rspec"
|
65
|
+
- ".ruby-version"
|
66
|
+
- CHANGELOG.md
|
67
|
+
- LICENSE
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- equilibrium
|
71
|
+
- exe/equilibrium
|
72
|
+
- lib/equilibrium.rb
|
73
|
+
- lib/equilibrium/analyzer.rb
|
74
|
+
- lib/equilibrium/catalog_builder.rb
|
75
|
+
- lib/equilibrium/cli.rb
|
76
|
+
- lib/equilibrium/registry_client.rb
|
77
|
+
- lib/equilibrium/schema_validator.rb
|
78
|
+
- lib/equilibrium/schemas/analyzer_output.rb
|
79
|
+
- lib/equilibrium/schemas/catalog.rb
|
80
|
+
- lib/equilibrium/schemas/expected_actual.rb
|
81
|
+
- lib/equilibrium/schemas/registry_api.rb
|
82
|
+
- lib/equilibrium/semantic_version.rb
|
83
|
+
- lib/equilibrium/summary_formatter.rb
|
84
|
+
- lib/equilibrium/tag_processor.rb
|
85
|
+
- lib/equilibrium/version.rb
|
86
|
+
- tmp/.gitkeep
|
87
|
+
homepage: https://github.com/TonyCTHsu/equilibrium
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata:
|
91
|
+
homepage_uri: https://github.com/TonyCTHsu/equilibrium
|
92
|
+
source_code_uri: https://github.com/TonyCTHsu/equilibrium
|
93
|
+
changelog_uri: https://github.com/TonyCTHsu/equilibrium/blob/master/CHANGELOG.md
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 3.0.0
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubygems_version: 3.4.19
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Container image tag validation tool
|
113
|
+
test_files: []
|