equilibrium 0.1.0 → 0.2.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -3
  3. data/README.md +41 -37
  4. data/lib/equilibrium/analyzer.rb +16 -14
  5. data/lib/equilibrium/canonical_version_mapper.rb +19 -0
  6. data/lib/equilibrium/catalog_builder.rb +31 -15
  7. data/lib/equilibrium/cli.rb +17 -172
  8. data/lib/equilibrium/commands/actual_command.rb +36 -0
  9. data/lib/equilibrium/commands/analyze_command.rb +49 -0
  10. data/lib/equilibrium/commands/catalog_command.rb +38 -0
  11. data/lib/equilibrium/commands/expected_command.rb +36 -0
  12. data/lib/equilibrium/commands/uncatalog_command.rb +42 -0
  13. data/lib/equilibrium/commands/version_command.rb +16 -0
  14. data/lib/equilibrium/mixins/error_handling.rb +34 -0
  15. data/lib/equilibrium/mixins/input_output.rb +40 -0
  16. data/lib/equilibrium/registry_client.rb +57 -45
  17. data/lib/equilibrium/repository_tags_service.rb +32 -0
  18. data/lib/equilibrium/repository_url_validator.rb +14 -0
  19. data/lib/equilibrium/schema_validator.rb +10 -2
  20. data/lib/equilibrium/schemas/analyzer_output.rb +8 -2
  21. data/lib/equilibrium/schemas/catalog.rb +16 -9
  22. data/lib/equilibrium/schemas/expected_actual.rb +1 -1
  23. data/lib/equilibrium/schemas/registry_api.rb +1 -1
  24. data/lib/equilibrium/summary_formatter.rb +1 -1
  25. data/lib/equilibrium/tag_data_builder.rb +16 -0
  26. data/lib/equilibrium/tag_processor.rb +24 -14
  27. data/lib/equilibrium/tag_sorter.rb +39 -0
  28. data/lib/equilibrium/tags_operation_service.rb +32 -0
  29. data/lib/equilibrium/version.rb +1 -1
  30. data/lib/equilibrium.rb +1 -1
  31. metadata +18 -4
  32. data/lib/equilibrium/semantic_version.rb +0 -24
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../mixins/error_handling"
6
+ require_relative "../mixins/input_output"
7
+ require_relative "../schema_validator"
8
+ require_relative "../schemas/expected_actual"
9
+ require_relative "../schemas/analyzer_output"
10
+ require_relative "../analyzer"
11
+
12
+ module Equilibrium
13
+ module Commands
14
+ # Command for analyzing expected vs actual tags and generating remediation plan
15
+ class AnalyzeCommand
16
+ include Mixins::ErrorHandling
17
+ include Mixins::InputOutput
18
+
19
+ # Execute the analyze command
20
+ # @param options [Hash] Command options (expected, actual, format, etc.)
21
+ def execute(options = {})
22
+ with_error_handling do
23
+ # Load and validate data files
24
+ expected_data = load_and_validate_json_file(options[:expected], error_prefix: "Expected data schema validation failed")
25
+ actual_data = load_and_validate_json_file(options[:actual], error_prefix: "Actual data schema validation failed")
26
+
27
+ # Perform analysis
28
+ analysis = Analyzer.analyze(expected_data, actual_data)
29
+
30
+ # Validate output schema
31
+ SchemaValidator.validate!(analysis, Equilibrium::Schemas::ANALYZER_OUTPUT, error_prefix: "Analyzer output schema validation failed")
32
+
33
+ # Format and display output
34
+ format_output(analysis, options[:format] || "summary", "analysis")
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def load_and_validate_json_file(file_path, error_prefix:)
41
+ raise "File not found: #{file_path}" unless File.exist?(file_path)
42
+
43
+ JSON.parse(File.read(file_path)).tap do |json|
44
+ SchemaValidator.validate!(json, Equilibrium::Schemas::EXPECTED_ACTUAL, error_prefix: error_prefix)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../mixins/error_handling"
6
+ require_relative "../mixins/input_output"
7
+ require_relative "../schema_validator"
8
+ require_relative "../schemas/expected_actual"
9
+ require_relative "../catalog_builder"
10
+
11
+ module Equilibrium
12
+ module Commands
13
+ # Command for converting expected tags JSON to catalog format
14
+ class CatalogCommand
15
+ include Mixins::ErrorHandling
16
+ include Mixins::InputOutput
17
+
18
+ # Execute the catalog command
19
+ # @param file_path [String, nil] Optional file path, uses stdin if nil
20
+ def execute(file_path = nil)
21
+ with_error_handling do
22
+ # Read input from file or stdin
23
+ input = read_input_data(file_path, "No input provided. Use: equilibrium expected registry | equilibrium catalog")
24
+
25
+ # Parse and validate JSON
26
+ data = JSON.parse(input)
27
+ SchemaValidator.validate!(data, Equilibrium::Schemas::EXPECTED_ACTUAL, error_prefix: "Schema validation failed")
28
+
29
+ # Convert to catalog format
30
+ catalog = CatalogBuilder.build_catalog(data)
31
+
32
+ # Output as JSON
33
+ puts JSON.pretty_generate(catalog)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mixins/error_handling"
4
+ require_relative "../mixins/input_output"
5
+ require_relative "../repository_url_validator"
6
+ require_relative "../schema_validator"
7
+ require_relative "../schemas/expected_actual"
8
+ require_relative "../tags_operation_service"
9
+
10
+ module Equilibrium
11
+ module Commands
12
+ # Command for generating expected mutable tags based on semantic versions
13
+ class ExpectedCommand
14
+ include Mixins::ErrorHandling
15
+ include Mixins::InputOutput
16
+
17
+ # Execute the expected command
18
+ # @param registry [String] Repository URL
19
+ # @param options [Hash] Command options (format, etc.)
20
+ def execute(registry, options = {})
21
+ with_error_handling do
22
+ full_repository_url = RepositoryUrlValidator.validate(registry)
23
+
24
+ # Generate complete expected output using high-level service
25
+ output = TagsOperationService.generate_expected_output(full_repository_url)
26
+
27
+ # Validate output against schema before writing
28
+ SchemaValidator.validate!(output, Equilibrium::Schemas::EXPECTED_ACTUAL, error_prefix: "Schema validation failed")
29
+
30
+ # Format and display output
31
+ format_output(output, options[:format] || "summary", "expected")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../mixins/error_handling"
6
+ require_relative "../mixins/input_output"
7
+ require_relative "../schema_validator"
8
+ require_relative "../schemas/catalog"
9
+ require_relative "../schemas/expected_actual"
10
+ require_relative "../catalog_builder"
11
+
12
+ module Equilibrium
13
+ module Commands
14
+ # Command for converting catalog format back to expected/actual format
15
+ class UncatalogCommand
16
+ include Mixins::ErrorHandling
17
+ include Mixins::InputOutput
18
+
19
+ # Execute the uncatalog command
20
+ # @param file_path [String, nil] Optional file path, uses stdin if nil
21
+ def execute(file_path = nil)
22
+ with_error_handling do
23
+ # Read input from file or stdin
24
+ input = read_input_data(file_path, "No input provided. Use: equilibrium catalog registry | equilibrium uncatalog")
25
+
26
+ # Parse and validate JSON
27
+ data = JSON.parse(input)
28
+ SchemaValidator.validate!(data, Equilibrium::Schemas::CATALOG, error_prefix: "Catalog schema validation failed")
29
+
30
+ # Convert back to expected/actual format
31
+ result = CatalogBuilder.reverse_catalog(data)
32
+
33
+ # Validate output before printing
34
+ SchemaValidator.validate!(result, Equilibrium::Schemas::EXPECTED_ACTUAL, error_prefix: "Output validation failed")
35
+
36
+ # Output as JSON
37
+ puts JSON.pretty_generate(result)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../version"
4
+
5
+ module Equilibrium
6
+ module Commands
7
+ # Command for displaying version information
8
+ class VersionCommand
9
+ # Execute the version command
10
+ def self.execute
11
+ puts "Equilibrium v#{Equilibrium::VERSION}"
12
+ puts "Container tag validation tool"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "thor"
5
+ require_relative "../schema_validator"
6
+
7
+ module Equilibrium
8
+ module Mixins
9
+ # Module for consistent error handling across all commands
10
+ module ErrorHandling
11
+ def with_error_handling(&block)
12
+ block.call
13
+ rescue Thor::Error
14
+ raise # Let Thor::Error bubble up for validation errors
15
+ rescue RegistryClient::Error => e
16
+ raise StandardError, e.message # Convert for test compatibility
17
+ rescue SchemaValidator::ValidationError => e
18
+ error_and_exit(e.message)
19
+ rescue JSON::ParserError => e
20
+ error_and_exit("Invalid JSON input: #{e.message}")
21
+ rescue => e
22
+ error_and_exit(e.message)
23
+ end
24
+
25
+ private
26
+
27
+ # Exit with error message
28
+ def error_and_exit(message, usage = nil)
29
+ warn message
30
+ exit 1
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../summary_formatter"
5
+
6
+ module Equilibrium
7
+ module Mixins
8
+ # Module for input/output operations - reading files, parsing JSON, formatting output
9
+ module InputOutput
10
+ def read_input_data(file_path = nil, usage_message = "No input provided")
11
+ input = file_path ? File.read(file_path) : $stdin.read
12
+ input = input.strip
13
+
14
+ raise usage_message if input.empty?
15
+ input
16
+ end
17
+
18
+ def format_output(data, format, summary_type = nil)
19
+ case format
20
+ when "json"
21
+ puts JSON.pretty_generate(data)
22
+ when "summary"
23
+ if summary_type
24
+ formatter = SummaryFormatter.new
25
+ case summary_type
26
+ when "expected", "actual"
27
+ formatter.print_tags_summary(data, summary_type)
28
+ when "analysis"
29
+ formatter.print_analysis_summary(data)
30
+ end
31
+ else
32
+ puts JSON.pretty_generate(data)
33
+ end
34
+ else
35
+ puts JSON.pretty_generate(data)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -26,29 +26,40 @@ module Equilibrium
26
26
  # by some registries (like GCR). Most registries follow the Docker Registry v2 spec
27
27
  # which only returns 'name' and 'tags' fields. To get digest information, separate
28
28
  # calls to /v2/<name>/manifests/<tag> are required per the official specification.
29
+ #
30
+ # PAGINATION ANALYSIS (August 2025):
31
+ # Current implementation fetches ALL tags in single API call. Analysis of production registries:
32
+ # - apm-inject: 29 tags
33
+ # - dd-lib-dotnet-init: 31 tags
34
+ # - dd-lib-java-init: 18 tags
35
+ # - dd-lib-js-init: 56 tags (largest)
36
+ # - dd-lib-php-init: 10 tags
37
+ # - dd-lib-python-init: 27 tags
38
+ # - dd-lib-ruby-init: 19 tags
39
+ # Total: 190 tags across all registries
40
+ #
41
+ # GCR PAGINATION RESEARCH FINDINGS:
42
+ # - DOCUMENTED LIMIT: 10,000 items for format-specific API requests
43
+ # Source: https://cloud.google.com/artifact-registry/quotas
44
+ # - NO PAGINATION SUPPORT: GCR ignores Docker Registry v2 pagination parameters ('n', 'last')
45
+ # Source: https://stackoverflow.com/questions/38307259 (unresolved since 2016)
46
+ # - ALL-OR-NOTHING: Returns complete tag lists up to 10k limit, then truncates unpredictably
47
+ # - NO PER-PAGE LIMIT: Not documented, not applicable due to lack of pagination support
48
+ #
49
+ # RECOMMENDATION: Current single-request approach is sufficient.
50
+ # Implementation complexity for pagination: 3-4 hours with minimal benefit.
51
+ # Consider only if individual repositories approach thousands of tags.
29
52
  class RegistryClient
30
53
  class Error < StandardError; end
31
54
 
32
- def list_tags(registry)
33
- url = build_api_url(registry)
34
- data = fetch_tags_data(url)
35
- parse_response(data)
36
- end
37
-
38
- private
39
-
40
- def build_api_url(registry)
41
- parts = registry.split("/")
42
- raise Error, "Invalid GCR registry format: #{registry}" if parts.length < 3
43
-
44
- host, project, *image = parts
55
+ attr_reader :uri
45
56
 
46
- "https://#{host}/v2/#{project}/#{image.join("/")}/tags/list"
57
+ def initialize(registry)
58
+ @registry = registry
59
+ @uri = URI(build_api_url(registry))
47
60
  end
48
61
 
49
- def fetch_tags_data(url)
50
- uri = URI(url)
51
-
62
+ def tagged_digests
52
63
  http = Net::HTTP.new(uri.host, uri.port)
53
64
  http.use_ssl = true
54
65
  http.read_timeout = 30
@@ -58,44 +69,45 @@ module Equilibrium
58
69
 
59
70
  raise Error, "API request failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
60
71
 
61
- data = JSON.parse(response.body)
62
- validate_registry_response(data)
63
- data
64
- rescue JSON::ParserError => e
65
- raise Error, "Invalid JSON response: #{e.message}"
66
- rescue => e
67
- raise Error, "Request failed: #{e.message}"
68
- end
69
-
70
- def validate_registry_response(data)
71
- SchemaValidator.validate!(data, Equilibrium::Schemas::REGISTRY_API_RESPONSE, error_prefix: "Registry API response validation failed")
72
- rescue SchemaValidator::ValidationError => e
73
- raise Error, e.message
74
- end
72
+ data = JSON.parse(response.body).tap do |json|
73
+ SchemaValidator.validate!(json, Equilibrium::Schemas::REGISTRY_API_RESPONSE, error_prefix: "Registry API response validation failed")
74
+ end
75
75
 
76
- def parse_response(data)
77
76
  # Data is already validated by schema, safe to process
78
- tags = data["tags"]
77
+ all_tags = data["tags"]
79
78
 
80
79
  # The 'manifest' field is a non-standard extension provided by some registries (like GCR)
81
80
  # Most registries (GHCR, Docker Hub, ECR) only return 'name' and 'tags' per Docker Registry v2 spec
82
81
  # When manifest data is missing, digest information is not available from the tags endpoint
83
- tag_to_digest = build_digest_mapping(data["manifest"] || {})
82
+ manifests = data.fetch("manifest")
84
83
 
85
- tags.each_with_object({}) do |tag, result|
86
- # Map tags to their digests. For registries without manifest data, digest will be nil
87
- # This is expected behavior - digest info requires separate manifest API calls per tag
88
- result[tag] = tag_to_digest[tag]
89
- end
90
- end
91
-
92
- def build_digest_mapping(manifests)
93
84
  # Build mapping from tag names to their SHA256 digests
94
85
  # Only works when registry provides non-standard 'manifest' field in tags response
95
- manifests.each_with_object({}) do |(digest, manifest_info), mapping|
96
- tags = manifest_info["tag"]
97
- tags.each { |tag| mapping[tag] = digest }
86
+ tag_to_digest = manifests.each_with_object({}) do |(digest, manifest_info), mapping|
87
+ tags_for_digest = manifest_info["tag"]
88
+ tags_for_digest.each { |tag| mapping[tag] = digest }
98
89
  end
90
+
91
+ # Validate that all_tags from API response matches tag_to_digest keys
92
+ all_tags_set = all_tags.to_set
93
+ manifest_tags_set = tag_to_digest.keys.to_set
94
+
95
+ unless all_tags_set == manifest_tags_set
96
+ raise Error, "Tag mismatch: API tags #{all_tags_set.to_a.sort} != manifest tags #{manifest_tags_set.to_a.sort}"
97
+ end
98
+
99
+ tag_to_digest
100
+ end
101
+
102
+ private
103
+
104
+ def build_api_url(registry)
105
+ parts = registry.split("/")
106
+ raise Error, "Invalid GCR registry format: #{registry}" if parts.length < 3
107
+
108
+ host, project, *image = parts
109
+
110
+ "https://#{host}/v2/#{project}/#{image.join("/")}/tags/list"
99
111
  end
100
112
  end
101
113
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry_client"
4
+ require_relative "tag_processor"
5
+ require_relative "canonical_version_mapper"
6
+
7
+ module Equilibrium
8
+ class RepositoryTagsService
9
+ def self.generate_expected_tags(repository_url)
10
+ registry_client = RegistryClient.new(repository_url)
11
+
12
+ tagged_digests = registry_client.tagged_digests
13
+ semantic_tags = TagProcessor.filter_semantic_tags(tagged_digests)
14
+ TagProcessor.compute_virtual_tags(semantic_tags)
15
+ end
16
+
17
+ def self.generate_actual_tags(repository_url)
18
+ registry_client = RegistryClient.new(repository_url)
19
+
20
+ tagged_digests = registry_client.tagged_digests
21
+ semantic_tags = TagProcessor.filter_semantic_tags(tagged_digests)
22
+
23
+ mutable_tags = TagProcessor.filter_mutable_tags(tagged_digests)
24
+ canonical_versions = CanonicalVersionMapper.map_to_canonical_versions(mutable_tags, semantic_tags)
25
+
26
+ {
27
+ "digests" => mutable_tags,
28
+ "canonical_versions" => canonical_versions
29
+ }
30
+ end
31
+ end
32
+ end
@@ -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
- schemer = JSONSchemer.schema(schema)
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,6 +8,7 @@ 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": {},
@@ -20,6 +21,7 @@ module Equilibrium
20
21
  # Example output (requires remediation):
21
22
  # {
22
23
  # "repository_url": "gcr.io/datadoghq/example",
24
+ # "repository_name": "example",
23
25
  # "expected_count": 3,
24
26
  # "actual_count": 2,
25
27
  # "missing_tags": {
@@ -49,9 +51,13 @@ module Equilibrium
49
51
  "title" => "Equilibrium Analyzer Output Schema",
50
52
  "description" => "Schema for output from 'equilibrium analyze --format=json' command",
51
53
  "type" => "object",
52
- "required" => ["repository_url", "expected_count", "actual_count", "missing_tags", "unexpected_tags", "mismatched_tags", "status", "remediation_plan"],
54
+ "required" => [
55
+ "repository_url",
56
+ "repository_name", "expected_count", "actual_count", "missing_tags", "unexpected_tags", "mismatched_tags", "status", "remediation_plan"
57
+ ],
53
58
  "properties" => {
54
59
  "repository_url" => {"type" => "string"},
60
+ "repository_name" => {"type" => "string"},
55
61
  "expected_count" => {"type" => "integer", "minimum" => 0},
56
62
  "actual_count" => {"type" => "integer", "minimum" => 0},
57
63
  "status" => {"enum" => ["perfect", "missing_tags", "mismatched", "extra_tags"]},
@@ -84,7 +90,7 @@ module Equilibrium
84
90
  "type" => "array",
85
91
  "items" => {
86
92
  "type" => "object",
87
- "required" => ["action", "tag", "command"],
93
+ "required" => ["action", "tag"],
88
94
  "properties" => {
89
95
  "action" => {"enum" => ["create_tag", "update_tag", "remove_tag"]},
90
96
  "tag" => {"type" => "string"},
@@ -3,25 +3,24 @@
3
3
  module Equilibrium
4
4
  module Schemas
5
5
  # JSON Schema for catalog output format
6
- # Used by `equilibrium catalog` command
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"
@@ -3,7 +3,7 @@
3
3
  module Equilibrium
4
4
  module Schemas
5
5
  # JSON Schema for expected/actual command output format
6
- # Used by `equilibrium expected` and `equilibrium actual` commands
6
+ # Used by `expected`, `actual` and `uncatalog` commands
7
7
  #
8
8
  # Example output:
9
9
  # {
@@ -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",
@@ -104,7 +104,7 @@ module Equilibrium
104
104
 
105
105
  # Create table data: [["Tag", "Version", "Digest"]]
106
106
  table_data = [["Tag", "Version", "Digest"]]
107
- mutable_tags.keys.sort.each do |tag|
107
+ mutable_tags.keys.each do |tag|
108
108
  canonical_version = canonical_versions[tag]
109
109
  digest = mutable_tags[tag]
110
110
  table_data << [tag, canonical_version, digest]
@@ -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