equilibrium 0.1.1 → 0.3.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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +29 -13
- data/README.md +97 -56
- data/lib/equilibrium/analyzer.rb +32 -50
- data/lib/equilibrium/canonical_version_mapper.rb +19 -0
- data/lib/equilibrium/catalog_builder.rb +31 -15
- data/lib/equilibrium/cli.rb +18 -181
- data/lib/equilibrium/commands/actual_command.rb +36 -0
- data/lib/equilibrium/commands/analyze_command.rb +49 -0
- data/lib/equilibrium/commands/catalog_command.rb +38 -0
- data/lib/equilibrium/commands/expected_command.rb +36 -0
- data/lib/equilibrium/commands/uncatalog_command.rb +42 -0
- data/lib/equilibrium/commands/version_command.rb +16 -0
- data/lib/equilibrium/mixins/error_handling.rb +34 -0
- data/lib/equilibrium/mixins/input_output.rb +40 -0
- data/lib/equilibrium/registry_client.rb +34 -45
- data/lib/equilibrium/repository_tags_service.rb +32 -0
- data/lib/equilibrium/repository_url_validator.rb +14 -0
- data/lib/equilibrium/schema_validator.rb +10 -2
- data/lib/equilibrium/schemas/analyzer_output.rb +34 -32
- data/lib/equilibrium/schemas/catalog.rb +16 -9
- data/lib/equilibrium/schemas/expected_actual.rb +1 -1
- data/lib/equilibrium/schemas/registry_api.rb +1 -1
- data/lib/equilibrium/summary_formatter.rb +11 -11
- data/lib/equilibrium/tag_data_builder.rb +16 -0
- data/lib/equilibrium/tag_processor.rb +24 -10
- data/lib/equilibrium/tag_sorter.rb +2 -16
- data/lib/equilibrium/tags_operation_service.rb +32 -0
- data/lib/equilibrium/version.rb +1 -1
- data/lib/equilibrium.rb +0 -1
- metadata +18 -10
- data/lib/equilibrium/semantic_version.rb +0 -24
- data/tmp/.gitkeep +0 -2
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
module Equilibrium
|
6
|
+
module RepositoryUrlValidator
|
7
|
+
def self.validate(repository_url)
|
8
|
+
unless repository_url.include?("/")
|
9
|
+
raise Thor::Error, "Repository URL must be full format (e.g., 'gcr.io/project-id/image-name'), not '#{repository_url}'"
|
10
|
+
end
|
11
|
+
repository_url
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -14,8 +14,7 @@ module Equilibrium
|
|
14
14
|
# @param error_prefix [String] Prefix for error messages (optional)
|
15
15
|
# @raise [ValidationError] If validation fails
|
16
16
|
def self.validate!(data, schema, error_prefix: "Schema validation failed")
|
17
|
-
|
18
|
-
errors = schemer.validate(data).to_a
|
17
|
+
errors = validate(data, schema)
|
19
18
|
|
20
19
|
return if errors.empty?
|
21
20
|
|
@@ -25,5 +24,14 @@ module Equilibrium
|
|
25
24
|
|
26
25
|
raise ValidationError, "#{error_prefix}:\n#{error_messages.join("\n")}"
|
27
26
|
end
|
27
|
+
|
28
|
+
# Validates data against a JSON Schema and returns validation errors
|
29
|
+
# @param data [Hash] The data to validate
|
30
|
+
# @param schema [Hash] The JSON Schema to validate against
|
31
|
+
# @return [Array] Array of validation errors (empty if valid)
|
32
|
+
def self.validate(data, schema)
|
33
|
+
schemer = JSONSchemer.schema(schema)
|
34
|
+
schemer.validate(data).to_a
|
35
|
+
end
|
28
36
|
end
|
29
37
|
end
|
@@ -8,25 +8,32 @@ module Equilibrium
|
|
8
8
|
# Example output (perfect equilibrium):
|
9
9
|
# {
|
10
10
|
# "repository_url": "gcr.io/datadoghq/apm-inject",
|
11
|
+
# "repository_name": "apm-inject",
|
11
12
|
# "expected_count": 28,
|
12
13
|
# "actual_count": 28,
|
13
14
|
# "missing_tags": {},
|
14
15
|
# "unexpected_tags": {},
|
15
16
|
# "mismatched_tags": {},
|
16
|
-
# "status": "perfect"
|
17
|
-
# "remediation_plan": []
|
17
|
+
# "status": "perfect"
|
18
18
|
# }
|
19
19
|
#
|
20
|
-
# Example output (
|
20
|
+
# Example output (with discrepancies):
|
21
21
|
# {
|
22
22
|
# "repository_url": "gcr.io/datadoghq/example",
|
23
|
+
# "repository_name": "example",
|
23
24
|
# "expected_count": 3,
|
24
25
|
# "actual_count": 2,
|
25
26
|
# "missing_tags": {
|
26
|
-
# "latest":
|
27
|
+
# "latest": {
|
28
|
+
# "expected": "sha256:abc123ef456789...",
|
29
|
+
# "actual": ""
|
30
|
+
# }
|
27
31
|
# },
|
28
32
|
# "unexpected_tags": {
|
29
|
-
# "dev":
|
33
|
+
# "dev": {
|
34
|
+
# "expected": "",
|
35
|
+
# "actual": "sha256:xyz789ab123456..."
|
36
|
+
# }
|
30
37
|
# },
|
31
38
|
# "mismatched_tags": {
|
32
39
|
# "0.1": {
|
@@ -34,37 +41,47 @@ module Equilibrium
|
|
34
41
|
# "actual": "sha256:old123ef456789..."
|
35
42
|
# }
|
36
43
|
# },
|
37
|
-
# "status": "missing_tags"
|
38
|
-
# "remediation_plan": [
|
39
|
-
# {
|
40
|
-
# "action": "create_tag",
|
41
|
-
# "tag": "latest",
|
42
|
-
# "digest": "sha256:abc123ef456789...",
|
43
|
-
# "command": "gcloud container images add-tag gcr.io/datadoghq/example@sha256:abc123... gcr.io/datadoghq/example:latest"
|
44
|
-
# }
|
45
|
-
# ]
|
44
|
+
# "status": "missing_tags"
|
46
45
|
# }
|
47
46
|
ANALYZER_OUTPUT = {
|
48
47
|
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
49
48
|
"title" => "Equilibrium Analyzer Output Schema",
|
50
49
|
"description" => "Schema for output from 'equilibrium analyze --format=json' command",
|
51
50
|
"type" => "object",
|
52
|
-
"required" => [
|
51
|
+
"required" => [
|
52
|
+
"repository_url",
|
53
|
+
"repository_name", "expected_count", "actual_count", "missing_tags", "unexpected_tags", "mismatched_tags", "status"
|
54
|
+
],
|
53
55
|
"properties" => {
|
54
56
|
"repository_url" => {"type" => "string"},
|
57
|
+
"repository_name" => {"type" => "string"},
|
55
58
|
"expected_count" => {"type" => "integer", "minimum" => 0},
|
56
59
|
"actual_count" => {"type" => "integer", "minimum" => 0},
|
57
60
|
"status" => {"enum" => ["perfect", "missing_tags", "mismatched", "extra_tags"]},
|
58
61
|
"missing_tags" => {
|
59
62
|
"type" => "object",
|
60
63
|
"patternProperties" => {
|
61
|
-
".*" => {
|
64
|
+
".*" => {
|
65
|
+
"type" => "object",
|
66
|
+
"required" => ["expected", "actual"],
|
67
|
+
"properties" => {
|
68
|
+
"expected" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
|
69
|
+
"actual" => {"type" => "string", "enum" => [""]}
|
70
|
+
}
|
71
|
+
}
|
62
72
|
}
|
63
73
|
},
|
64
74
|
"unexpected_tags" => {
|
65
75
|
"type" => "object",
|
66
76
|
"patternProperties" => {
|
67
|
-
".*" => {
|
77
|
+
".*" => {
|
78
|
+
"type" => "object",
|
79
|
+
"required" => ["expected", "actual"],
|
80
|
+
"properties" => {
|
81
|
+
"expected" => {"type" => "string", "enum" => [""]},
|
82
|
+
"actual" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"}
|
83
|
+
}
|
84
|
+
}
|
68
85
|
}
|
69
86
|
},
|
70
87
|
"mismatched_tags" => {
|
@@ -79,21 +96,6 @@ module Equilibrium
|
|
79
96
|
}
|
80
97
|
}
|
81
98
|
}
|
82
|
-
},
|
83
|
-
"remediation_plan" => {
|
84
|
-
"type" => "array",
|
85
|
-
"items" => {
|
86
|
-
"type" => "object",
|
87
|
-
"required" => ["action", "tag", "command"],
|
88
|
-
"properties" => {
|
89
|
-
"action" => {"enum" => ["create_tag", "update_tag", "remove_tag"]},
|
90
|
-
"tag" => {"type" => "string"},
|
91
|
-
"digest" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
|
92
|
-
"old_digest" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
|
93
|
-
"new_digest" => {"type" => "string", "pattern" => "^sha256:[a-f0-9]{64}$"},
|
94
|
-
"command" => {"type" => "string"}
|
95
|
-
}
|
96
|
-
}
|
97
99
|
}
|
98
100
|
}
|
99
101
|
}.freeze
|
@@ -3,25 +3,24 @@
|
|
3
3
|
module Equilibrium
|
4
4
|
module Schemas
|
5
5
|
# JSON Schema for catalog output format
|
6
|
-
# Used by `
|
6
|
+
# Used by `catalog` command
|
7
7
|
#
|
8
8
|
# Example output:
|
9
9
|
# {
|
10
|
+
# "repository_url": "gcr.io/datadoghq/apm-inject",
|
11
|
+
# "repository_name": "apm-inject",
|
10
12
|
# "images": [
|
11
13
|
# {
|
12
|
-
# "name": "apm-inject",
|
13
14
|
# "tag": "latest",
|
14
15
|
# "digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
15
16
|
# "canonical_version": "0.43.2"
|
16
17
|
# },
|
17
18
|
# {
|
18
|
-
# "name": "apm-inject",
|
19
19
|
# "tag": "0",
|
20
20
|
# "digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
21
21
|
# "canonical_version": "0.43.2"
|
22
22
|
# },
|
23
23
|
# {
|
24
|
-
# "name": "apm-inject",
|
25
24
|
# "tag": "0.43",
|
26
25
|
# "digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
|
27
26
|
# "canonical_version": "0.43.1"
|
@@ -33,22 +32,30 @@ module Equilibrium
|
|
33
32
|
"title" => "Tag Resolver Schema",
|
34
33
|
"description" => "Schema for Resolve tag",
|
35
34
|
"type" => "object",
|
35
|
+
"required" => ["repository_url", "repository_name", "images"],
|
36
36
|
"properties" => {
|
37
|
+
"repository_url" => {
|
38
|
+
"type" => "string",
|
39
|
+
"description" => "Full repository URL (e.g., 'gcr.io/project-id/image-name')",
|
40
|
+
"minLength" => 1,
|
41
|
+
"pattern" => "^[a-zA-Z0-9.-]+(/[a-zA-Z0-9._-]+)*$"
|
42
|
+
},
|
43
|
+
"repository_name" => {
|
44
|
+
"type" => "string",
|
45
|
+
"description" => "Repository name extracted from URL (e.g., 'apm-inject')",
|
46
|
+
"minLength" => 1,
|
47
|
+
"pattern" => "^[a-zA-Z0-9._-]+$"
|
48
|
+
},
|
37
49
|
"images" => {
|
38
50
|
"type" => "array",
|
39
51
|
"items" => {
|
40
52
|
"type" => "object",
|
41
53
|
"required" => [
|
42
|
-
"name",
|
43
54
|
"tag",
|
44
55
|
"digest",
|
45
56
|
"canonical_version"
|
46
57
|
],
|
47
58
|
"properties" => {
|
48
|
-
"name" => {
|
49
|
-
"type" => "string",
|
50
|
-
"description" => "The name of the image"
|
51
|
-
},
|
52
59
|
"tag" => {
|
53
60
|
"type" => "string",
|
54
61
|
"description" => "The mutable tag of the image"
|
@@ -24,7 +24,7 @@ module Equilibrium
|
|
24
24
|
"title" => "Docker Registry v2 Tags List Response Schema",
|
25
25
|
"description" => "Schema for response from Docker Registry v2 API /v2/{name}/tags/list endpoint",
|
26
26
|
"type" => "object",
|
27
|
-
"required" => ["name", "tags"],
|
27
|
+
"required" => ["name", "tags", "manifest"],
|
28
28
|
"properties" => {
|
29
29
|
"name" => {
|
30
30
|
"type" => "string",
|
@@ -43,9 +43,9 @@ module Equilibrium
|
|
43
43
|
has_issues = true
|
44
44
|
say "Missing Tags (should be created):"
|
45
45
|
missing_table = [["Tag", "Should Point To"]]
|
46
|
-
analysis[:missing_tags].each do |tag,
|
47
|
-
|
48
|
-
missing_table << [tag,
|
46
|
+
analysis[:missing_tags].each do |tag, details|
|
47
|
+
full_digest = details[:expected] || "unknown"
|
48
|
+
missing_table << [tag, full_digest]
|
49
49
|
end
|
50
50
|
print_table(missing_table, borders: true)
|
51
51
|
say ""
|
@@ -58,10 +58,10 @@ module Equilibrium
|
|
58
58
|
mismatched_table = [["Tag", "Expected", "Actual"]]
|
59
59
|
analysis[:mismatched_tags].each do |tag, details|
|
60
60
|
if details.is_a?(Hash)
|
61
|
-
expected = details[:expected]
|
62
|
-
actual = details[:actual]
|
61
|
+
expected = details[:expected] || "unknown"
|
62
|
+
actual = details[:actual] || "unknown"
|
63
63
|
else
|
64
|
-
expected = details
|
64
|
+
expected = details || "unknown"
|
65
65
|
actual = "unknown"
|
66
66
|
end
|
67
67
|
mismatched_table << [tag, expected, actual]
|
@@ -75,17 +75,17 @@ module Equilibrium
|
|
75
75
|
has_issues = true
|
76
76
|
say "Unexpected Tags (should be removed):"
|
77
77
|
unexpected_table = [["Tag", "Currently Points To"]]
|
78
|
-
analysis[:unexpected_tags].each do |tag,
|
79
|
-
|
80
|
-
unexpected_table << [tag,
|
78
|
+
analysis[:unexpected_tags].each do |tag, details|
|
79
|
+
full_digest = details[:actual] || "unknown"
|
80
|
+
unexpected_table << [tag, full_digest]
|
81
81
|
end
|
82
82
|
print_table(unexpected_table, borders: true)
|
83
83
|
say ""
|
84
84
|
end
|
85
85
|
|
86
86
|
if has_issues
|
87
|
-
say "To see detailed
|
88
|
-
say " equilibrium analyze --expected expected.json --actual actual.json --format=json
|
87
|
+
say "To see detailed analysis data, use:"
|
88
|
+
say " equilibrium analyze --expected expected.json --actual actual.json --format=json"
|
89
89
|
else
|
90
90
|
say "✓ Registry is in perfect equilibrium!", :green
|
91
91
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tag_sorter"
|
4
|
+
|
5
|
+
module Equilibrium
|
6
|
+
class TagDataBuilder
|
7
|
+
def self.build_output(repository_url, repository_name, digests, canonical_versions)
|
8
|
+
{
|
9
|
+
"repository_url" => repository_url,
|
10
|
+
"repository_name" => repository_name,
|
11
|
+
"digests" => TagSorter.sort_descending(digests),
|
12
|
+
"canonical_versions" => TagSorter.sort_descending(canonical_versions)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -8,6 +8,14 @@ module Equilibrium
|
|
8
8
|
new.compute_virtual_tags(semantic_tags)
|
9
9
|
end
|
10
10
|
|
11
|
+
def self.filter_semantic_tags(tagged_digests)
|
12
|
+
new.filter_semantic_tags(tagged_digests)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.filter_mutable_tags(tagged_digests)
|
16
|
+
new.filter_mutable_tags(tagged_digests)
|
17
|
+
end
|
18
|
+
|
11
19
|
def compute_virtual_tags(semantic_tags)
|
12
20
|
return {"digests" => {}, "canonical_versions" => {}} if semantic_tags.empty?
|
13
21
|
|
@@ -55,26 +63,32 @@ module Equilibrium
|
|
55
63
|
{"digests" => digests, "canonical_versions" => canonical_versions}
|
56
64
|
end
|
57
65
|
|
58
|
-
def filter_semantic_tags(
|
59
|
-
|
60
|
-
all_tags.select { |tag, _| semantic_version?(tag) }
|
66
|
+
def filter_semantic_tags(tagged_digests)
|
67
|
+
tagged_digests.select { |tag, _| semantic_version?(tag) }
|
61
68
|
end
|
62
69
|
|
63
|
-
def filter_mutable_tags(
|
64
|
-
|
65
|
-
all_tags.select { |tag, _| mutable_tag?(tag) }
|
70
|
+
def filter_mutable_tags(tagged_digests)
|
71
|
+
tagged_digests.select { |tag, _| mutable_tag?(tag) }
|
66
72
|
end
|
67
73
|
|
68
74
|
private
|
69
75
|
|
70
76
|
def semantic_version?(tag)
|
71
|
-
|
77
|
+
# Strictly validate MAJOR.MINOR.PATCH format where:
|
78
|
+
# - MAJOR, MINOR, PATCH are non-negative integers
|
79
|
+
# - No leading zeros (except for '0' itself)
|
80
|
+
# - No prefixes (like 'v1.2.3', 'release-1.2.3')
|
81
|
+
# - No suffixes (like '1.2.3-alpha', '1.2.3+build')
|
82
|
+
# - No prereleases (like '1.2.3-rc.1', '1.2.3-beta.2')
|
83
|
+
tag.match?(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/)
|
72
84
|
end
|
73
85
|
|
74
86
|
def mutable_tag?(tag)
|
75
|
-
|
76
|
-
|
77
|
-
|
87
|
+
# Validate mutable tags: latest, major versions (digits only), or major.minor format
|
88
|
+
# - 'latest' is the special case for the highest overall version
|
89
|
+
# - Major versions: non-negative integers without leading zeros (e.g., '1', '0', '42')
|
90
|
+
# - Minor versions: MAJOR.MINOR format with same zero-leading rules (e.g., '1.2', '0.9')
|
91
|
+
tag.match?(/^(latest|(0|[1-9]\d*)(\.(0|[1-9]\d*))?)$/)
|
78
92
|
end
|
79
93
|
end
|
80
94
|
end
|
@@ -4,10 +4,6 @@ module Equilibrium
|
|
4
4
|
class TagSorter
|
5
5
|
# Sort tags in descending version order: latest first, then major versions (descending), then minor versions (descending)
|
6
6
|
def self.sort_descending(tags_hash)
|
7
|
-
new.sort_descending(tags_hash)
|
8
|
-
end
|
9
|
-
|
10
|
-
def sort_descending(tags_hash)
|
11
7
|
return {} if tags_hash.nil? || tags_hash.empty?
|
12
8
|
|
13
9
|
sorted = {}
|
@@ -20,10 +16,10 @@ module Equilibrium
|
|
20
16
|
# Sort other tags by version (descending)
|
21
17
|
other_tags = tags_hash.keys.reject { |k| k == "latest" }
|
22
18
|
sorted_tags = other_tags.sort_by do |tag|
|
23
|
-
if
|
19
|
+
if tag.match?(/^[0-9]+$/)
|
24
20
|
# Major version: sort by numeric value (descending)
|
25
21
|
[-tag.to_i]
|
26
|
-
elsif
|
22
|
+
elsif tag.match?(/^[0-9]+\.[0-9]+$/)
|
27
23
|
# Minor version: sort by version (descending)
|
28
24
|
parts = tag.split(".").map(&:to_i)
|
29
25
|
[-parts[0], -parts[1]]
|
@@ -39,15 +35,5 @@ module Equilibrium
|
|
39
35
|
|
40
36
|
sorted
|
41
37
|
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def major_version?(tag)
|
46
|
-
tag.match?(/^[0-9]+$/)
|
47
|
-
end
|
48
|
-
|
49
|
-
def minor_version?(tag)
|
50
|
-
tag.match?(/^[0-9]+\.[0-9]+$/)
|
51
|
-
end
|
52
38
|
end
|
53
39
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "repository_tags_service"
|
4
|
+
require_relative "tag_data_builder"
|
5
|
+
|
6
|
+
module Equilibrium
|
7
|
+
class TagsOperationService
|
8
|
+
def self.generate_expected_output(repository_url)
|
9
|
+
tag_data = RepositoryTagsService.generate_expected_tags(repository_url)
|
10
|
+
repository_name = repository_url.split("/").last
|
11
|
+
|
12
|
+
TagDataBuilder.build_output(
|
13
|
+
repository_url,
|
14
|
+
repository_name,
|
15
|
+
tag_data["digests"],
|
16
|
+
tag_data["canonical_versions"]
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.generate_actual_output(repository_url)
|
21
|
+
tag_data = RepositoryTagsService.generate_actual_tags(repository_url)
|
22
|
+
repository_name = repository_url.split("/").last
|
23
|
+
|
24
|
+
TagDataBuilder.build_output(
|
25
|
+
repository_url,
|
26
|
+
repository_name,
|
27
|
+
tag_data["digests"],
|
28
|
+
tag_data["canonical_versions"]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/equilibrium/version.rb
CHANGED
data/lib/equilibrium.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: equilibrium
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Hsu
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: thor
|
@@ -71,29 +70,39 @@ files:
|
|
71
70
|
- exe/equilibrium
|
72
71
|
- lib/equilibrium.rb
|
73
72
|
- lib/equilibrium/analyzer.rb
|
73
|
+
- lib/equilibrium/canonical_version_mapper.rb
|
74
74
|
- lib/equilibrium/catalog_builder.rb
|
75
75
|
- lib/equilibrium/cli.rb
|
76
|
+
- lib/equilibrium/commands/actual_command.rb
|
77
|
+
- lib/equilibrium/commands/analyze_command.rb
|
78
|
+
- lib/equilibrium/commands/catalog_command.rb
|
79
|
+
- lib/equilibrium/commands/expected_command.rb
|
80
|
+
- lib/equilibrium/commands/uncatalog_command.rb
|
81
|
+
- lib/equilibrium/commands/version_command.rb
|
82
|
+
- lib/equilibrium/mixins/error_handling.rb
|
83
|
+
- lib/equilibrium/mixins/input_output.rb
|
76
84
|
- lib/equilibrium/registry_client.rb
|
85
|
+
- lib/equilibrium/repository_tags_service.rb
|
86
|
+
- lib/equilibrium/repository_url_validator.rb
|
77
87
|
- lib/equilibrium/schema_validator.rb
|
78
88
|
- lib/equilibrium/schemas/analyzer_output.rb
|
79
89
|
- lib/equilibrium/schemas/catalog.rb
|
80
90
|
- lib/equilibrium/schemas/expected_actual.rb
|
81
91
|
- lib/equilibrium/schemas/registry_api.rb
|
82
|
-
- lib/equilibrium/semantic_version.rb
|
83
92
|
- lib/equilibrium/summary_formatter.rb
|
93
|
+
- lib/equilibrium/tag_data_builder.rb
|
84
94
|
- lib/equilibrium/tag_processor.rb
|
85
95
|
- lib/equilibrium/tag_sorter.rb
|
96
|
+
- lib/equilibrium/tags_operation_service.rb
|
86
97
|
- lib/equilibrium/version.rb
|
87
|
-
- tmp/.gitkeep
|
88
98
|
homepage: https://github.com/TonyCTHsu/equilibrium
|
89
99
|
licenses:
|
90
100
|
- MIT
|
91
101
|
metadata:
|
92
102
|
homepage_uri: https://github.com/TonyCTHsu/equilibrium
|
93
103
|
source_code_uri: https://github.com/TonyCTHsu/equilibrium
|
94
|
-
changelog_uri: https://github.com/TonyCTHsu/equilibrium/blob/
|
95
|
-
github_repo: ssh://github.com/TonyCTHsu/
|
96
|
-
post_install_message:
|
104
|
+
changelog_uri: https://github.com/TonyCTHsu/equilibrium/blob/v0.3.0/CHANGELOG.md
|
105
|
+
github_repo: ssh://github.com/TonyCTHsu/equilibrium
|
97
106
|
rdoc_options: []
|
98
107
|
require_paths:
|
99
108
|
- lib
|
@@ -108,8 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
117
|
- !ruby/object:Gem::Version
|
109
118
|
version: '0'
|
110
119
|
requirements: []
|
111
|
-
rubygems_version: 3.
|
112
|
-
signing_key:
|
120
|
+
rubygems_version: 3.6.9
|
113
121
|
specification_version: 4
|
114
122
|
summary: Container image tag validation tool
|
115
123
|
test_files: []
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Equilibrium
|
4
|
-
module SemanticVersion
|
5
|
-
# Strictly validate MAJOR.MINOR.PATCH format where:
|
6
|
-
# - MAJOR, MINOR, PATCH are non-negative integers
|
7
|
-
# - No leading zeros (except for '0' itself)
|
8
|
-
# - No prefixes (like 'v1.2.3', 'release-1.2.3')
|
9
|
-
# - No suffixes (like '1.2.3-alpha', '1.2.3+build')
|
10
|
-
# - No prereleases (like '1.2.3-rc.1', '1.2.3-beta.2')
|
11
|
-
def self.valid?(tag)
|
12
|
-
# Strict regex: each component must be either '0' or a number without leading zeros
|
13
|
-
return false unless tag.match?(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/)
|
14
|
-
|
15
|
-
# Additional validation: ensure it's a valid Gem::Version
|
16
|
-
begin
|
17
|
-
Gem::Version.new(tag)
|
18
|
-
true
|
19
|
-
rescue ArgumentError
|
20
|
-
false
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
data/tmp/.gitkeep
DELETED