pangea-core 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.
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'dry-struct'
18
+ require 'pangea/resources/types'
19
+
20
+ module Pangea
21
+ module Resources
22
+ # Base computed attributes - common to all resources
23
+ class BaseComputedAttributes
24
+ attr_reader :resource_ref
25
+
26
+ def initialize(resource_ref)
27
+ @resource_ref = resource_ref
28
+ end
29
+
30
+ # Common terraform attributes available on all resources
31
+ def id
32
+ resource_ref.ref(:id)
33
+ end
34
+
35
+ def terraform_resource_name
36
+ "#{resource_ref.type}.#{resource_ref.name}"
37
+ end
38
+
39
+ def tags
40
+ resource_ref.resource_attributes[:tags] || {}
41
+ end
42
+ end
43
+
44
+ # Resource reference object returned by resource functions
45
+ # Provides access to resource attributes, outputs, and computed properties
46
+ class ResourceReference < Dry::Struct
47
+ # Registry for provider-specific computed attributes classes
48
+ @@computed_attributes_registry = {}
49
+
50
+ # Normalize input keys: accept `attributes:` as alias for `resource_attributes:`
51
+ transform_keys do |key|
52
+ k = key.to_sym
53
+ k == :attributes ? :resource_attributes : k
54
+ end
55
+
56
+ attribute :type, Types::Coercible::String # aws_vpc, aws_subnet, etc. (coerces Symbol → String)
57
+ attribute :name, Types::Symbol | Types::String # Resource name
58
+ # Accept Hash or Dry::Struct (auto-coerced via constructor)
59
+ attribute :resource_attributes, Types::Hash.constructor { |v|
60
+ v.respond_to?(:to_h) && !v.is_a?(Hash) ? v.to_h : v
61
+ }
62
+ attribute :outputs, Types::Hash.default({}.freeze) # Available outputs for this resource type
63
+ attribute? :computed_properties, Types::Hash.optional # Resource-specific computed properties
64
+ attribute? :computed, Types::Hash.optional # Alias for computed_properties
65
+
66
+ # Register computed attributes classes for resource types
67
+ # @param mapping [Hash] resource_type => computed_attributes_class
68
+ def self.register_computed_attributes(mapping)
69
+ @@computed_attributes_registry.merge!(mapping)
70
+ end
71
+
72
+ # Alias for `type` — some callers prefer `resource_type` to avoid conflicts
73
+ def resource_type
74
+ type.to_sym
75
+ end
76
+
77
+ # Generate terraform reference for any attribute
78
+ def ref(attribute_name)
79
+ "${#{type}.#{name}.#{attribute_name}}"
80
+ end
81
+
82
+ # Alias for ref - more natural syntax
83
+ def [](attribute_name)
84
+ ref(attribute_name)
85
+ end
86
+
87
+ # Access to common outputs with friendly names
88
+ def id
89
+ ref(:id)
90
+ end
91
+
92
+ def arn
93
+ ref(:arn)
94
+ end
95
+
96
+ # Resource-specific computed properties (extensible via register_computed_attributes)
97
+ def computed_attributes
98
+ @computed_attributes ||= begin
99
+ klass = @@computed_attributes_registry[type]
100
+ klass ? klass.new(self) : BaseComputedAttributes.new(self)
101
+ end
102
+ end
103
+
104
+ # Method delegation to outputs, computed properties, and computed attributes
105
+ def method_missing(method_name, *args, &block)
106
+ if outputs.key?(method_name)
107
+ outputs[method_name]
108
+ elsif computed_properties&.key?(method_name)
109
+ computed_properties[method_name]
110
+ elsif computed&.key?(method_name)
111
+ computed[method_name]
112
+ elsif computed_attributes.respond_to?(method_name)
113
+ computed_attributes.public_send(method_name, *args, &block)
114
+ else
115
+ super
116
+ end
117
+ end
118
+
119
+ def respond_to_missing?(method_name, include_private = false)
120
+ outputs.key?(method_name) ||
121
+ computed_properties&.key?(method_name) ||
122
+ computed&.key?(method_name) ||
123
+ computed_attributes.respond_to?(method_name, include_private) ||
124
+ super
125
+ end
126
+
127
+ # Convert to hash for terraform-synthesizer integration
128
+ def to_h
129
+ {
130
+ type: type,
131
+ name: name,
132
+ attributes: resource_attributes, # Use 'attributes' as key for compatibility
133
+ outputs: outputs
134
+ }
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'dry-struct'
17
+ require 'dry-types'
18
+ require 'json'
19
+ require 'base64'
20
+
21
+ module Pangea
22
+ module Resources
23
+ # Common types for resource definitions
24
+ module Types
25
+ include Dry.Types()
26
+
27
+ # Provider type registries — provider gems register their Types module here
28
+ # so T::SomeProviderType resolves to AWS::Types::SomeProviderType etc.
29
+ @provider_type_modules = []
30
+
31
+ def self.register_provider_types(mod)
32
+ @provider_type_modules << mod unless @provider_type_modules.include?(mod)
33
+ end
34
+
35
+ def self.const_missing(name)
36
+ if name == :ResourceReference
37
+ require 'pangea/resources/reference' unless defined?(::Pangea::Resources::ResourceReference)
38
+ return ::Pangea::Resources::ResourceReference
39
+ end
40
+
41
+ # Search registered provider type modules
42
+ @provider_type_modules.each do |mod|
43
+ return mod.const_get(name) if mod.const_defined?(name, false)
44
+ end
45
+
46
+ super
47
+ end
48
+
49
+ # Provider-agnostic shared types
50
+
51
+ # CIDR block validation (e.g., "10.0.0.0/16")
52
+ CidrBlock = String.constrained(format: /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}\z/)
53
+
54
+ # Domain name validation
55
+ DomainName = String.constrained(
56
+ format: /\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/i
57
+ )
58
+
59
+ # Wildcard domain name validation
60
+ WildcardDomainName = String.constrained(
61
+ format: /\A\*\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/i
62
+ )
63
+
64
+ # Email address validation
65
+ EmailAddress = String.constrained(format: /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/)
66
+
67
+ # Network port (0-65535)
68
+ Port = Integer.constrained(gteq: 0, lteq: 65535)
69
+
70
+ # Network protocols
71
+ IpProtocol = String.enum('tcp', 'udp', 'icmp', 'icmpv6', 'all', '-1')
72
+
73
+ # Port range
74
+ PortRange = Hash.schema(from_port: Port, to_port: Port)
75
+
76
+ # POSIX permissions (octal format)
77
+ PosixPermissions = String.constrained(format: /\A[0-7]{3,4}\z/)
78
+
79
+ # Unix User/Group IDs
80
+ UnixUserId = Integer.constrained(gteq: 0, lteq: 4294967295)
81
+ UnixGroupId = Integer.constrained(gteq: 0, lteq: 4294967295)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Core types (must be loaded first)
17
+ require_relative 'core'
18
+
19
+ # Provider types are loaded by their respective gems:
20
+ # pangea-aws, pangea-cloudflare, pangea-hcloud
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Load all modular type definitions
17
+ require_relative 'types/index'
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2025 The Pangea Authors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'net/http'
17
+ require 'json'
18
+ require 'timeout'
19
+
20
+ module Pangea
21
+ module Utilities
22
+ # IP Discovery service for finding public IP addresses
23
+ class IpDiscovery
24
+ # Service definitions with parsers
25
+ SERVICES = [
26
+ {
27
+ name: 'ipify',
28
+ url: 'https://api.ipify.org?format=json',
29
+ parser: ->(body) { JSON.parse(body)['ip'] }
30
+ },
31
+ {
32
+ name: 'ipinfo',
33
+ url: 'https://ipinfo.io/ip',
34
+ parser: ->(body) { body.strip }
35
+ },
36
+ {
37
+ name: 'aws_checkip',
38
+ url: 'https://checkip.amazonaws.com',
39
+ parser: ->(body) { body.strip }
40
+ },
41
+ {
42
+ name: 'ifconfig_me',
43
+ url: 'https://ifconfig.me',
44
+ parser: ->(body) { body.strip }
45
+ }
46
+ ].freeze
47
+
48
+ IP_REGEX = /\A(?:\d{1,3}\.){3}\d{1,3}\z/.freeze
49
+
50
+ attr_reader :timeout, :logger
51
+
52
+ def initialize(timeout: 5, logger: nil)
53
+ @timeout = timeout
54
+ @logger = logger || Logger.new(STDOUT)
55
+ end
56
+
57
+ # Discover public IP address from multiple services
58
+ def discover
59
+ SERVICES.each do |service|
60
+ ip = try_service(service)
61
+ return ip if ip
62
+ end
63
+
64
+ raise DiscoveryError, "Failed to discover public IP from any service"
65
+ end
66
+
67
+ # Try a single service with timeout
68
+ def try_service(service)
69
+ Timeout.timeout(@timeout) do
70
+ uri = URI(service[:url])
71
+ response = Net::HTTP.get_response(uri)
72
+
73
+ if response.is_a?(Net::HTTPSuccess)
74
+ ip = service[:parser].call(response.body)
75
+
76
+ if validate_ip_format(ip)
77
+ @logger&.info("[IpDiscovery] Discovered public IP from #{service[:name]}: #{ip}")
78
+ return ip
79
+ else
80
+ @logger&.warn("[IpDiscovery] Invalid IP format from #{service[:name]}: #{ip}")
81
+ end
82
+ else
83
+ @logger&.warn("[IpDiscovery] HTTP error from #{service[:name]}: #{response.code}")
84
+ end
85
+ end
86
+
87
+ nil
88
+ rescue Timeout::Error
89
+ @logger&.warn("[IpDiscovery] Timeout querying #{service[:name]}")
90
+ nil
91
+ rescue StandardError => e
92
+ @logger&.warn("[IpDiscovery] Error querying #{service[:name]}: #{e.message}")
93
+ nil
94
+ end
95
+
96
+ private
97
+
98
+ # Validate IP address format
99
+ def validate_ip_format(ip)
100
+ return false unless ip.match?(IP_REGEX)
101
+
102
+ octets = ip.split('.')
103
+ octets.all? { |octet| octet.to_i <= 255 }
104
+ end
105
+ end
106
+
107
+ class DiscoveryError < StandardError; end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PangeaCore
4
+ VERSION = %(0.1.0).freeze
5
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'dry-types'
5
+ require 'terraform-synthesizer'
6
+
7
+ # dry-types 1.9+ ConstraintError requires 2 args (message, input).
8
+ # Our resource type code uses `raise ConstraintError, "msg"` which passes
9
+ # only 1 arg via Ruby's `exception(msg)` method. Patch to accept single arg.
10
+ if Dry::Types::ConstraintError.instance_method(:initialize).arity.abs > 1
11
+ class Dry::Types::ConstraintError
12
+ alias_method :_orig_initialize, :initialize
13
+ def initialize(message, input = nil)
14
+ _orig_initialize(message, input)
15
+ end
16
+ end
17
+ end
18
+
19
+ # Minimal ActiveSupport-like extensions used by resource code
20
+ unless Object.method_defined?(:present?)
21
+ class Object
22
+ def present?
23
+ respond_to?(:empty?) ? !empty? : !nil?
24
+ end
25
+
26
+ def blank?
27
+ respond_to?(:empty?) ? empty? : nil?
28
+ end
29
+ end
30
+
31
+ class NilClass
32
+ def present? = false
33
+ def blank? = true
34
+ end
35
+
36
+ class FalseClass
37
+ def present? = false
38
+ def blank? = true
39
+ end
40
+
41
+ class TrueClass
42
+ def present? = true
43
+ def blank? = false
44
+ end
45
+
46
+ class String
47
+ def present? = !empty?
48
+ def blank? = empty? || strip.empty?
49
+ end
50
+ end
51
+
52
+ # Core types
53
+ require_relative 'pangea/resources/types'
54
+ require_relative 'pangea/resource_registry'
55
+ require_relative 'pangea/resources/helpers'
56
+ require_relative 'pangea/resources/base'
57
+ require_relative 'pangea/resources/base_attributes'
58
+ require_relative 'pangea/resources/reference'
59
+ require_relative 'pangea/resources/network_helpers'
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path(%(lib), __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require_relative %(lib/pangea-core/version)
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = %(pangea-core)
9
+ spec.version = PangeaCore::VERSION
10
+ spec.authors = [%(Luis Zayas)]
11
+ spec.email = [%(drzthslnt@gmail.com)]
12
+ spec.description = %(Core types and utilities for Pangea infrastructure DSL. Provides ResourceReference, ResourceRegistry, Base, Types, and Helpers shared across all provider gems.)
13
+ spec.summary = %(Core types for Pangea infrastructure DSL)
14
+ spec.homepage = %(https://github.com/pleme-io/pangea-core)
15
+ spec.license = %(Apache-2.0)
16
+ spec.require_paths = [%(lib)]
17
+ spec.required_ruby_version = %(>=3.3.0)
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+
23
+ spec.add_dependency "terraform-synthesizer", "~> 0.0.28"
24
+ spec.add_dependency "dry-types", "~> 1.7"
25
+ spec.add_dependency "dry-struct", "~> 1.6"
26
+ spec.add_dependency "base64"
27
+
28
+ spec.add_development_dependency "rspec", "~> 3.12"
29
+ spec.add_development_dependency "rake", "~> 13.0"
30
+ spec.add_development_dependency "simplecov", "~> 0.22"
31
+
32
+ spec.metadata['rubygems_mfa_required'] = 'true'
33
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pangea-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Luis Zayas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: terraform-synthesizer
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.28
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.28
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-types
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-struct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: base64
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.12'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.12'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.22'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.22'
111
+ description: Core types and utilities for Pangea infrastructure DSL. Provides ResourceReference,
112
+ ResourceRegistry, Base, Types, and Helpers shared across all provider gems.
113
+ email:
114
+ - drzthslnt@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - Gemfile
121
+ - Gemfile.lock
122
+ - LICENSE
123
+ - Rakefile
124
+ - flake.lock
125
+ - flake.nix
126
+ - gemset.nix
127
+ - lib/pangea-core.rb
128
+ - lib/pangea-core/version.rb
129
+ - lib/pangea/resource_registry.rb
130
+ - lib/pangea/resources/base.rb
131
+ - lib/pangea/resources/base_attributes.rb
132
+ - lib/pangea/resources/helpers.rb
133
+ - lib/pangea/resources/network_helpers.rb
134
+ - lib/pangea/resources/reference.rb
135
+ - lib/pangea/resources/types.rb
136
+ - lib/pangea/resources/types/core.rb
137
+ - lib/pangea/resources/types/index.rb
138
+ - lib/pangea/utilities/ip_discovery.rb
139
+ - pangea-core.gemspec
140
+ homepage: https://github.com/pleme-io/pangea-core
141
+ licenses:
142
+ - Apache-2.0
143
+ metadata:
144
+ rubygems_mfa_required: 'true'
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: 3.3.0
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubygems_version: 3.3.26
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: Core types for Pangea infrastructure DSL
164
+ test_files: []