pangea-core 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.
- checksums.yaml +4 -4
- data/flake.lock +3 -3
- data/flake.nix +7 -41
- data/lib/pangea/component_registry.rb +44 -0
- data/lib/pangea/components/base.rb +63 -0
- data/lib/pangea/entities/module_definition.rb +120 -0
- data/lib/pangea/entities/namespace.rb +144 -0
- data/lib/pangea/entities/project.rb +84 -0
- data/lib/pangea/entities/template.rb +103 -0
- data/lib/pangea/entities.rb +26 -0
- data/lib/pangea/errors.rb +124 -0
- data/lib/pangea/logging/formatters.rb +90 -0
- data/lib/pangea/logging/structured_logger.rb +186 -0
- data/lib/pangea/logging.rb +22 -0
- data/lib/pangea/resources/base_attributes.rb +11 -0
- data/lib/pangea/resources/builders/output_builder.rb +113 -0
- data/lib/pangea/resources/resource_builder.rb +109 -0
- data/lib/pangea/resources/validators/format_validators.rb +58 -0
- data/lib/pangea/resources/validators/network_validators.rb +79 -0
- data/lib/pangea/testing/indifferent_hash.rb +69 -0
- data/lib/pangea/testing/mock_resource_reference.rb +70 -0
- data/lib/pangea/testing/mock_terraform_synthesizer.rb +57 -0
- data/lib/pangea/testing/resource_examples.rb +53 -0
- data/lib/pangea/testing/spec_setup.rb +57 -0
- data/lib/pangea/testing/synthesis_test_helpers.rb +171 -0
- data/lib/pangea/testing.rb +22 -0
- data/lib/pangea/types/base_types.rb +53 -0
- data/lib/pangea/types/domain_types.rb +114 -0
- data/lib/pangea/types/registry.rb +70 -0
- data/lib/pangea/validation.rb +101 -0
- data/lib/pangea-core/version.rb +1 -1
- data/lib/pangea-core.rb +26 -1
- data/pangea-core.gemspec +1 -1
- metadata +35 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5531883112f0813ec3c3fb9261c15dbed5ad56fa1c1243e6060a493a64a89af8
|
|
4
|
+
data.tar.gz: 4061c17da43c2e93204e0b0ed28504ee9a7ee2acf306f5c1564e9469fe4846ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a6d5530c63c03213627f0385f9beff6d09b726c60a00068d9721f0ba2e81190098b431f52ee8dd17609fdafc8e49ec8883548895dd640dbf55dbd89cdfe3e6f
|
|
7
|
+
data.tar.gz: fec9537295beb3213a1536b6a51c057fca7efcb4c0578bf28f950f0cb6934c03eb1aae627f3d517b8589e5f235970d5dc058bd5d2a18ea14d71c0c4fedc77d3e
|
data/flake.lock
CHANGED
|
@@ -771,11 +771,11 @@
|
|
|
771
771
|
]
|
|
772
772
|
},
|
|
773
773
|
"locked": {
|
|
774
|
-
"lastModified":
|
|
775
|
-
"narHash": "sha256-
|
|
774
|
+
"lastModified": 1771861648,
|
|
775
|
+
"narHash": "sha256-XjYnA7XQUFTg/XrgQL35oojiX/oddmuqHSsLbQg+X44=",
|
|
776
776
|
"owner": "pleme-io",
|
|
777
777
|
"repo": "substrate",
|
|
778
|
-
"rev": "
|
|
778
|
+
"rev": "0f97a626e8141f4ffa2a4e143f96df2689a82b6b",
|
|
779
779
|
"type": "github"
|
|
780
780
|
},
|
|
781
781
|
"original": {
|
data/flake.nix
CHANGED
|
@@ -16,45 +16,11 @@
|
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
outputs = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
...
|
|
27
|
-
}:
|
|
28
|
-
flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux" "aarch64-darwin"] (system: let
|
|
29
|
-
pkgs = import nixpkgs {
|
|
30
|
-
inherit system;
|
|
31
|
-
overlays = [ruby-nix.overlays.ruby];
|
|
32
|
-
};
|
|
33
|
-
rnix = ruby-nix.lib pkgs;
|
|
34
|
-
rnix-env = rnix {
|
|
35
|
-
name = "pangea-core";
|
|
36
|
-
gemset = ./gemset.nix;
|
|
37
|
-
};
|
|
38
|
-
env = rnix-env.env;
|
|
39
|
-
ruby = rnix-env.ruby;
|
|
40
|
-
|
|
41
|
-
rubyBuild = import "${substrate}/lib/ruby-build.nix" {
|
|
42
|
-
inherit pkgs;
|
|
43
|
-
forgeCmd = "${forge.packages.${system}.default}/bin/forge";
|
|
44
|
-
defaultGhcrToken = "";
|
|
45
|
-
};
|
|
46
|
-
in {
|
|
47
|
-
devShells.default = pkgs.mkShell {
|
|
48
|
-
buildInputs = [env ruby];
|
|
49
|
-
shellHook = ''
|
|
50
|
-
export RUBYLIB=$PWD/lib:$RUBYLIB
|
|
51
|
-
export DRY_TYPES_WARNINGS=false
|
|
52
|
-
'';
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
apps = rubyBuild.mkRubyGemApps {
|
|
56
|
-
srcDir = self;
|
|
57
|
-
name = "pangea-core";
|
|
58
|
-
};
|
|
59
|
-
});
|
|
19
|
+
outputs = { self, nixpkgs, ruby-nix, flake-utils, substrate, forge, ... }:
|
|
20
|
+
(import "${substrate}/lib/ruby-gem-flake.nix" {
|
|
21
|
+
inherit nixpkgs ruby-nix flake-utils substrate forge;
|
|
22
|
+
}) {
|
|
23
|
+
inherit self;
|
|
24
|
+
name = "pangea-core";
|
|
25
|
+
};
|
|
60
26
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
module Pangea
|
|
18
|
+
module ComponentRegistry
|
|
19
|
+
@components = []
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def register_component(component_module)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
unless @components.include?(component_module)
|
|
26
|
+
@components << component_module
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def registered_components
|
|
32
|
+
@mutex.synchronize { @components.dup }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear!
|
|
36
|
+
@mutex.synchronize { @components.clear }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def registered?(component_module)
|
|
40
|
+
@mutex.synchronize { @components.include?(component_module) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
module Pangea
|
|
18
|
+
module Components
|
|
19
|
+
module Base
|
|
20
|
+
class ComponentError < StandardError; end
|
|
21
|
+
class ValidationError < ComponentError; end
|
|
22
|
+
class CompositionError < ComponentError; end
|
|
23
|
+
|
|
24
|
+
def validate_required_attributes(attributes, required)
|
|
25
|
+
missing = required - attributes.keys
|
|
26
|
+
unless missing.empty?
|
|
27
|
+
raise ValidationError, "Missing required attributes: #{missing.join(', ')}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def calculate_subnet_cidr(vpc_cidr, index, new_bits = 8)
|
|
32
|
+
require 'ipaddr'
|
|
33
|
+
|
|
34
|
+
vpc_network = IPAddr.new(vpc_cidr)
|
|
35
|
+
vpc_prefix = vpc_cidr.split('/').last.to_i
|
|
36
|
+
subnet_prefix = vpc_prefix + new_bits
|
|
37
|
+
|
|
38
|
+
subnet_size = 2 ** (32 - subnet_prefix)
|
|
39
|
+
subnet_network = vpc_network.to_i + (index * subnet_size)
|
|
40
|
+
|
|
41
|
+
"#{IPAddr.new(subnet_network, Socket::AF_INET)}/#{subnet_prefix}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def component_resource_name(component_name, resource_type, suffix = nil)
|
|
45
|
+
parts = [component_name, resource_type]
|
|
46
|
+
parts << suffix if suffix
|
|
47
|
+
parts.join('_').to_sym
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def merge_tags(default_tags, user_tags = {})
|
|
51
|
+
default_tags.merge(user_tags)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def component_outputs(resources, computed = {})
|
|
55
|
+
{
|
|
56
|
+
resources: resources,
|
|
57
|
+
computed: computed,
|
|
58
|
+
created_at: Time.now.utc.iso8601
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
|
|
19
|
+
module Pangea
|
|
20
|
+
module Entities
|
|
21
|
+
class ModuleDefinition < Dry::Struct
|
|
22
|
+
module Type
|
|
23
|
+
RESOURCE = :resource
|
|
24
|
+
FUNCTION = :function
|
|
25
|
+
COMPOSITE = :composite
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attribute :name, Pangea::Types::ModuleName
|
|
29
|
+
attribute :version, Pangea::Types::Version.optional.default("0.0.1")
|
|
30
|
+
attribute :description, Pangea::Types::OptionalString.default(nil)
|
|
31
|
+
attribute :author, Pangea::Types::OptionalString.default(nil)
|
|
32
|
+
|
|
33
|
+
attribute :type, Pangea::Types::Strict::Symbol.default(:resource).enum(:resource, :function, :composite)
|
|
34
|
+
attribute :source, Pangea::Types::FilePath.optional.default(nil)
|
|
35
|
+
attribute :path, Pangea::Types::DirectoryPath.optional.default(nil)
|
|
36
|
+
|
|
37
|
+
attribute :inputs, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
38
|
+
attribute :outputs, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
39
|
+
attribute :dependencies, Pangea::Types::ModuleArray.default([].freeze)
|
|
40
|
+
|
|
41
|
+
attribute :ruby_version, Pangea::Types::Version.optional.default(nil)
|
|
42
|
+
attribute :required_gems, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
43
|
+
|
|
44
|
+
def resource_module?
|
|
45
|
+
type == Type::RESOURCE || type == Type::COMPOSITE
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def function_module?
|
|
49
|
+
type == Type::FUNCTION || type == Type::COMPOSITE
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load_path
|
|
53
|
+
return path if path
|
|
54
|
+
return File.dirname(source) if source
|
|
55
|
+
|
|
56
|
+
"modules/#{name}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def required_inputs
|
|
60
|
+
inputs.select { |_, config| config[:required] }.keys
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def optional_inputs
|
|
64
|
+
inputs.reject { |_, config| config[:required] }.keys
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_inputs(provided_inputs)
|
|
68
|
+
errors = []
|
|
69
|
+
provided = provided_inputs.keys.map(&:to_sym)
|
|
70
|
+
|
|
71
|
+
required_inputs.each do |input|
|
|
72
|
+
unless provided.include?(input)
|
|
73
|
+
errors << "Missing required input: #{input}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
provided.each do |input|
|
|
78
|
+
unless inputs.key?(input)
|
|
79
|
+
errors << "Unknown input: #{input}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
provided_inputs.each do |key, value|
|
|
84
|
+
if inputs[key.to_sym] && inputs[key.to_sym][:type]
|
|
85
|
+
expected_type = inputs[key.to_sym][:type]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise ValidationError, errors.join(", ") unless errors.empty?
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_documentation
|
|
94
|
+
doc = ["# Module: #{name}"]
|
|
95
|
+
doc << "Version: #{version}" if version
|
|
96
|
+
doc << "\n#{description}" if description
|
|
97
|
+
doc << "\nAuthor: #{author}" if author
|
|
98
|
+
|
|
99
|
+
if inputs.any?
|
|
100
|
+
doc << "\n## Inputs"
|
|
101
|
+
inputs.each do |name, config|
|
|
102
|
+
required = config[:required] ? " (required)" : ""
|
|
103
|
+
doc << "- `#{name}`#{required}: #{config[:description] || 'No description'}"
|
|
104
|
+
doc << " - Type: #{config[:type]}" if config[:type]
|
|
105
|
+
doc << " - Default: #{config[:default]}" if config[:default]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if outputs.any?
|
|
110
|
+
doc << "\n## Outputs"
|
|
111
|
+
outputs.each do |name, config|
|
|
112
|
+
doc << "- `#{name}`: #{config[:description] || 'No description'}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
doc.join("\n")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
|
|
19
|
+
module Pangea
|
|
20
|
+
module Entities
|
|
21
|
+
class Namespace < Dry::Struct
|
|
22
|
+
class StateConfig < Dry::Struct
|
|
23
|
+
attribute? :bucket, Pangea::Types::String
|
|
24
|
+
attribute? :key, Pangea::Types::String
|
|
25
|
+
attribute? :region, Pangea::Types::String.optional
|
|
26
|
+
attribute? :dynamodb_table, Pangea::Types::String
|
|
27
|
+
attribute? :lock, Pangea::Types::String
|
|
28
|
+
attribute? :encrypt, Pangea::Types::Bool
|
|
29
|
+
attribute? :path, Pangea::Types::String
|
|
30
|
+
|
|
31
|
+
def validate_s3!
|
|
32
|
+
errors = []
|
|
33
|
+
errors << "S3 bucket name is required" if bucket.nil?
|
|
34
|
+
errors << "S3 key is required" if key.nil?
|
|
35
|
+
lock_table = dynamodb_table || lock
|
|
36
|
+
errors << "DynamoDB lock table is required" if lock_table.nil?
|
|
37
|
+
raise ::Pangea::Entities::ValidationError, errors.join(", ") unless errors.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def lock_table
|
|
41
|
+
dynamodb_table || lock
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class State < Dry::Struct
|
|
46
|
+
attribute :type, Pangea::Types::StateBackendType
|
|
47
|
+
attribute :config, StateConfig
|
|
48
|
+
|
|
49
|
+
def self.new(attributes)
|
|
50
|
+
attrs = attributes.is_a?(Hash) ? attributes : {}
|
|
51
|
+
state_type = attrs[:type]
|
|
52
|
+
config_attrs = attrs[:config] || {}
|
|
53
|
+
|
|
54
|
+
if state_type == :s3
|
|
55
|
+
required_fields = [:bucket, :key]
|
|
56
|
+
missing_fields = required_fields.select { |field| config_attrs[field].nil? || config_attrs[field].empty? }
|
|
57
|
+
unless missing_fields.empty?
|
|
58
|
+
raise ::Pangea::Entities::ValidationError, "S3 backend requires: #{missing_fields.join(', ')}"
|
|
59
|
+
end
|
|
60
|
+
elsif state_type == :local
|
|
61
|
+
config_attrs[:path] ||= "./terraform.tfstate"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
super(attrs)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def s3?
|
|
68
|
+
type == :s3
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def local?
|
|
72
|
+
type == :local
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attribute :name, Pangea::Types::NamespaceString
|
|
77
|
+
attribute :state, State
|
|
78
|
+
attribute :description, Pangea::Types::OptionalString.default(nil)
|
|
79
|
+
attribute :tags, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
80
|
+
|
|
81
|
+
def s3_backend?
|
|
82
|
+
state.s3?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def local_backend?
|
|
86
|
+
state.local?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def state_config
|
|
90
|
+
config = {
|
|
91
|
+
type: state.type,
|
|
92
|
+
region: state.config.region
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if s3_backend?
|
|
96
|
+
config[:bucket] = state.config.bucket
|
|
97
|
+
config[:lock] = state.config.lock
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
config.compact
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def s3_config
|
|
104
|
+
raise "Namespace #{name} does not use S3 backend" unless s3_backend?
|
|
105
|
+
state.config.validate_s3!
|
|
106
|
+
{
|
|
107
|
+
bucket: state.config.bucket,
|
|
108
|
+
key: state.config.key,
|
|
109
|
+
region: state.config.region,
|
|
110
|
+
dynamodb_table: state.config.lock_table
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def to_terraform_backend
|
|
115
|
+
if local_backend?
|
|
116
|
+
return {
|
|
117
|
+
local: {
|
|
118
|
+
path: state.config.path || "terraform.tfstate"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if state.config.bucket.nil? || state.config.bucket.empty?
|
|
124
|
+
raise ValidationError, "S3 bucket is required but was nil or empty for namespace '#{name}'"
|
|
125
|
+
end
|
|
126
|
+
if state.config.key.nil? || state.config.key.empty?
|
|
127
|
+
raise ValidationError, "S3 key is required but was nil or empty for namespace '#{name}'"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
s3: {
|
|
132
|
+
bucket: state.config.bucket,
|
|
133
|
+
key: state.config.key,
|
|
134
|
+
region: state.config.region,
|
|
135
|
+
dynamodb_table: state.config.lock_table,
|
|
136
|
+
encrypt: state.config.encrypt
|
|
137
|
+
}.compact
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class ValidationError < StandardError; end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
|
|
19
|
+
module Pangea
|
|
20
|
+
module Entities
|
|
21
|
+
class Project < Dry::Struct
|
|
22
|
+
attribute :name, Pangea::Types::ProjectString
|
|
23
|
+
attribute :namespace, Pangea::Types::NamespaceString
|
|
24
|
+
attribute :site, Pangea::Types::SiteString.optional.default(nil)
|
|
25
|
+
attribute :description, Pangea::Types::OptionalString.default(nil)
|
|
26
|
+
|
|
27
|
+
attribute :modules, Pangea::Types::ModuleArray.default([].freeze)
|
|
28
|
+
attribute :variables, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
29
|
+
attribute :outputs, Pangea::Types::StringArray.default([].freeze)
|
|
30
|
+
attribute :depends_on, Pangea::Types::IdentifierArray.default([].freeze)
|
|
31
|
+
|
|
32
|
+
attribute :terraform_version, Pangea::Types::TerraformVersion.optional.default(nil)
|
|
33
|
+
attribute :tags, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
34
|
+
|
|
35
|
+
def full_name
|
|
36
|
+
[namespace, site, name].compact.join('.')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def state_key
|
|
40
|
+
parts = [namespace]
|
|
41
|
+
parts << site if site
|
|
42
|
+
parts << name
|
|
43
|
+
parts.join('/')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def has_modules?
|
|
47
|
+
!modules.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def has_dependencies?
|
|
51
|
+
!depends_on.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def module_config(module_name)
|
|
55
|
+
modules.find { |m| m == module_name }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_backend_config(prefix: nil)
|
|
59
|
+
key_parts = [prefix, state_key].compact
|
|
60
|
+
{
|
|
61
|
+
key: key_parts.join('/'),
|
|
62
|
+
workspace_key_prefix: "workspaces"
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate!
|
|
67
|
+
errors = []
|
|
68
|
+
|
|
69
|
+
if depends_on.include?(name)
|
|
70
|
+
errors << "Project cannot depend on itself"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
modules.each do |mod|
|
|
74
|
+
unless mod.match?(/\A[a-z][a-z0-9_-]*\z/)
|
|
75
|
+
errors << "Invalid module name: #{mod}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise ValidationError, errors.join(", ") unless errors.empty?
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
|
|
19
|
+
module Pangea
|
|
20
|
+
module Entities
|
|
21
|
+
class Template < Dry::Struct
|
|
22
|
+
attribute :name, Pangea::Types::Identifier
|
|
23
|
+
attribute :content, Pangea::Types::Strict::String
|
|
24
|
+
attribute :file_path, Pangea::Types::FilePath.optional.default(nil)
|
|
25
|
+
|
|
26
|
+
attribute :namespace, Pangea::Types::NamespaceString.optional.default(nil)
|
|
27
|
+
attribute :project, Pangea::Types::ProjectString.optional.default(nil)
|
|
28
|
+
attribute :variables, Pangea::Types::SymbolizedHash.default({}.freeze)
|
|
29
|
+
|
|
30
|
+
attribute :target_version, Pangea::Types::TerraformVersion.optional.default(nil)
|
|
31
|
+
attribute :strict_mode, Pangea::Types::Strict::Bool.default(false)
|
|
32
|
+
|
|
33
|
+
def source
|
|
34
|
+
file_path || "<inline:#{name}>"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def from_file?
|
|
38
|
+
!file_path.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cache_key
|
|
42
|
+
parts = [namespace, project, name].compact
|
|
43
|
+
parts.join('/')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate!
|
|
47
|
+
errors = []
|
|
48
|
+
|
|
49
|
+
if content.strip.empty?
|
|
50
|
+
errors << "Template content cannot be empty"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if content.include?("<%") || content.include?("{{")
|
|
54
|
+
errors << "Template appears to contain ERB or Mustache syntax (not supported)"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
raise ValidationError, errors.join(", ") unless errors.empty?
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def metadata
|
|
62
|
+
return {} unless content.start_with?("# @")
|
|
63
|
+
|
|
64
|
+
metadata = {}
|
|
65
|
+
content.lines.each do |line|
|
|
66
|
+
break unless line.start_with?("# @")
|
|
67
|
+
|
|
68
|
+
if line =~ /# @(\w+):\s*(.+)$/
|
|
69
|
+
key = $1.to_sym
|
|
70
|
+
value = $2.strip
|
|
71
|
+
metadata[key] = value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
metadata
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def content_without_metadata
|
|
79
|
+
return content unless content.start_with?("# @")
|
|
80
|
+
|
|
81
|
+
lines = content.lines
|
|
82
|
+
lines.drop_while { |line| line.start_with?("# @") }.join
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class CompilationResult < Dry::Struct
|
|
87
|
+
attribute :success, Pangea::Types::Strict::Bool
|
|
88
|
+
attribute :terraform_json, Pangea::Types::Strict::String.optional.default(nil)
|
|
89
|
+
attribute :errors, Pangea::Types::StringArray.default([].freeze)
|
|
90
|
+
attribute :warnings, Pangea::Types::StringArray.default([].freeze)
|
|
91
|
+
attribute :template_name, Pangea::Types::Strict::String.optional.default(nil)
|
|
92
|
+
attribute :template_count, Pangea::Types::Strict::Integer.optional.default(nil)
|
|
93
|
+
|
|
94
|
+
def success?
|
|
95
|
+
success && errors.empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def failure?
|
|
99
|
+
!success?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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_relative 'entities/namespace'
|
|
18
|
+
require_relative 'entities/project'
|
|
19
|
+
require_relative 'entities/module_definition'
|
|
20
|
+
require_relative 'entities/template'
|
|
21
|
+
|
|
22
|
+
module Pangea
|
|
23
|
+
module Entities
|
|
24
|
+
class ValidationError < StandardError; end
|
|
25
|
+
end
|
|
26
|
+
end
|