hecks 3.9.1 → 3.10.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/lib/hecks/deploy/bluebook/deploy.bluebook +106 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
- data/lib/hecks/deploy/oidc.json +16 -3
- data/lib/hecks/hecks/adapters/deploy_toolchain/clearing.rb +51 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
- data/lib/hecks/hecks/adapters/site_toolchain/role_prober.rb +78 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
- data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
- data/lib/hecks/hecks/hecks.hecksagon +8 -0
- data/lib/hecks/hecks/hecks.world +2 -1
- data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +15 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
- data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
- data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
- data/lib/hecks/projections/site/cms_editor/attributes.rb +75 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +64 -0
- data/lib/hecks/projections/site/cms_editor/media.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/pretty.rb +40 -0
- data/lib/hecks/projections/site/cms_editor/schema.rb +84 -0
- data/lib/hecks/projections/site/cms_editor/setting.rb +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +19 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +176 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +79 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +79 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +23 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +26 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +154 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +85 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +120 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +282 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +80 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +102 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +75 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +109 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +27 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +123 -0
- data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +16 -0
- data/lib/hecks/projections/site/cms_editor.rb +107 -0
- data/lib/hecks/projections/site/role_probe.rb +72 -0
- data/lib/hecks/site/bluebook/site.bluebook +28 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/tools/site_routes/cli.rb +1 -1
- data/lib/hecks/tools/site_routes/editor_files.rb +38 -0
- data/lib/hecks/tools/site_routes.rb +14 -7
- data/lib/hecks/version.rb +1 -1
- data/prebuilt/{verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json → verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json} +1 -1
- data/rust/Cargo.toml +3 -1
- data/rust/codegen/src/bridging.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +9 -9
- data/rust/codegen/src/main.rs +1 -1
- data/rust/codegen/src/mutations.rs +46 -1
- data/rust/codegen/src/naming.rs +30 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/src/kernel/expr.rs +7 -1
- data/rust/src/kernel/expression_operators/presence.rs +41 -3
- metadata +41 -5
- /data/prebuilt/{6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal → c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal} +0 -0
- /data/prebuilt/{0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal → cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ab2b4b582c53edd23595354a8db9d84a3a8ec4572d044a77ccfea2ac113c073
|
|
4
|
+
data.tar.gz: 80af1db86bd574227fc6a03e7d19b9fa1ede9d4fbf54c4934da9211c67a892a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d28b07fb0b22a96aa8a2dbe2481010c668555a18f256572ee67dc95afb5c517d833cc749718ca804cac68a1ce34e5268e755347ad2d32668ec264d8519d81b08
|
|
7
|
+
data.tar.gz: 6e823533f842634ad3771ccb994feda57439c9f027aa189e0ff36ba049024fe3c845fd96e3543ce720cb96d6bcaf159604a1279db640f4137ac1e72677d436da
|
|
@@ -680,6 +680,112 @@ Hecks.bluebook "Deploy" do
|
|
|
680
680
|
trigger Recipe::Fault, with: { refusal: :refusal }
|
|
681
681
|
end
|
|
682
682
|
|
|
683
|
+
aggregate "Handover" do
|
|
684
|
+
description "One clearing of a domain's deployment settings: the body of every deployed_to block in its .world emptied, so a project handed to a client holds none of the host's own region, stacks, secrets or domain."
|
|
685
|
+
|
|
686
|
+
attribute :domain, DomainPath
|
|
687
|
+
attribute :run, RunKey
|
|
688
|
+
attribute :out, Location, optional: true
|
|
689
|
+
attribute :output, Report, optional: true
|
|
690
|
+
attribute :refusal, Report, optional: true
|
|
691
|
+
identified_by :run
|
|
692
|
+
|
|
693
|
+
value_object "RunKey" do
|
|
694
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
695
|
+
|
|
696
|
+
invariant("a run is named") { !value.to_s.empty? }
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# A domain directory, whose `bluebook/` holds the `.world` that declares `deployed_to`.
|
|
700
|
+
value_object "DomainPath" do
|
|
701
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
702
|
+
|
|
703
|
+
invariant("a domain is named by its path") { !value.to_s.empty? }
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# A directory the cleared worlds are written to, at their paths below `bluebook/`, instead of
|
|
707
|
+
# over the originals.
|
|
708
|
+
value_object "Location" do
|
|
709
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
value_object "Report" do
|
|
713
|
+
attribute :value, String
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
lifecycle :status, default: "requested" do
|
|
717
|
+
transition "Complete" => "cleared", from: "requested"
|
|
718
|
+
transition "Fault" => "faulted", from: "requested"
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# `hecks deploy handover.clear <domain> [out=]`. The rest of the file is left as it was, a world
|
|
722
|
+
# with nothing to clear is answered as cleared with nothing removed, and a domain with no
|
|
723
|
+
# `.world` file is faulted with the sentence saying so.
|
|
724
|
+
command "Clear" do
|
|
725
|
+
goal "Empty the body of every deployed_to block in a domain's .world, keeping the settings' names in a comment"
|
|
726
|
+
|
|
727
|
+
attribute :domain, DomainPath
|
|
728
|
+
attribute :run, RunKey
|
|
729
|
+
attribute :out, Location, optional: true
|
|
730
|
+
|
|
731
|
+
sets :domain
|
|
732
|
+
sets :run
|
|
733
|
+
sets :out
|
|
734
|
+
|
|
735
|
+
emits "ClearingRequested"
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
command "Complete" do
|
|
739
|
+
goal "Record that the deployment settings were cleared, from which file and which settings"
|
|
740
|
+
|
|
741
|
+
reference_to Handover
|
|
742
|
+
attribute :output, Report
|
|
743
|
+
|
|
744
|
+
sets :output
|
|
745
|
+
|
|
746
|
+
emits "DeploymentCleared"
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
command "Fault" do
|
|
750
|
+
goal "Record that the deployment settings could not be cleared, and why"
|
|
751
|
+
|
|
752
|
+
reference_to Handover
|
|
753
|
+
attribute :refusal, Report
|
|
754
|
+
|
|
755
|
+
sets :refusal
|
|
756
|
+
|
|
757
|
+
emits "ClearingFaulted"
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
query "Outcome" do
|
|
761
|
+
description "What one clearing did: its status, and the file and settings removed or the reason it stopped."
|
|
762
|
+
attribute :run, RunKey
|
|
763
|
+
where(run: :run)
|
|
764
|
+
order_by :run
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
query "Faulted" do
|
|
768
|
+
description "Every clearing that could not be done."
|
|
769
|
+
where(status: "faulted")
|
|
770
|
+
order_by :run
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
policy "ClearWhenRequested" do
|
|
775
|
+
on "Handover.ClearingRequested"
|
|
776
|
+
trigger Handover::DeployToolchain::ClearDeployment, with: { run: :run }
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
policy "RecordTheClearing" do
|
|
780
|
+
on "Handover.HandoverAnswered"
|
|
781
|
+
trigger Handover::Complete, with: { output: :output }
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
policy "RecordTheFaultedClearing" do
|
|
785
|
+
on "Handover.HandoverRefused"
|
|
786
|
+
trigger Handover::Fault, with: { refusal: :refusal }
|
|
787
|
+
end
|
|
788
|
+
|
|
683
789
|
aggregate "MakefileCheck" do
|
|
684
790
|
description "One lint of generated deploy Makefiles for prod-touching recipes that hide failure."
|
|
685
791
|
|
|
@@ -27,6 +27,14 @@ Hecks.hecksagon "Deploy" do
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
Deploy::Handover.port "DeployToolchain" do
|
|
31
|
+
asks "ClearDeployment", to: Handover do
|
|
32
|
+
attribute :run, RunKey
|
|
33
|
+
answers "HandoverAnswered"
|
|
34
|
+
refuses "HandoverRefused"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
Deploy::MakefileCheck.port "DeployToolchain" do
|
|
31
39
|
asks "Scan", to: MakefileCheck do
|
|
32
40
|
attribute :run, RunKey
|
data/lib/hecks/deploy/oidc.json
CHANGED
|
@@ -156,6 +156,21 @@
|
|
|
156
156
|
"verb": "Deploy::FargateTarget.Declare",
|
|
157
157
|
"role": null
|
|
158
158
|
},
|
|
159
|
+
{
|
|
160
|
+
"scope": "deploy:handover.clear",
|
|
161
|
+
"verb": "Deploy::Handover.Clear",
|
|
162
|
+
"role": null
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"scope": "deploy:handover.complete",
|
|
166
|
+
"verb": "Deploy::Handover.Complete",
|
|
167
|
+
"role": null
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"scope": "deploy:handover.fault",
|
|
171
|
+
"verb": "Deploy::Handover.Fault",
|
|
172
|
+
"role": null
|
|
173
|
+
},
|
|
159
174
|
{
|
|
160
175
|
"scope": "deploy:lambda_target.declare",
|
|
161
176
|
"verb": "Deploy::LambdaTarget.Declare",
|
|
@@ -367,7 +382,5 @@
|
|
|
367
382
|
"role": null
|
|
368
383
|
}
|
|
369
384
|
],
|
|
370
|
-
"roles": [
|
|
371
|
-
|
|
372
|
-
]
|
|
385
|
+
"roles": []
|
|
373
386
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "../console_capture"
|
|
5
|
+
require_relative "../world_clearing"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
class DeployToolchain
|
|
10
|
+
# The `ClearDeployment` ask: empties the `deployed_to` blocks of a domain's `.world` files, in
|
|
11
|
+
# place or into a directory, and says which files and settings it removed.
|
|
12
|
+
module Clearing
|
|
13
|
+
# Empties every `deployed_to` block under `<domain>/bluebook/**/*.world`, through
|
|
14
|
+
# `hecks deploy handover.clear`.
|
|
15
|
+
#
|
|
16
|
+
# With `out`, the cleared files are written there at their paths below `<domain>/bluebook/`
|
|
17
|
+
# and the originals are left alone; without it they are rewritten in place. A world with
|
|
18
|
+
# nothing left to clear is not rewritten and not listed.
|
|
19
|
+
#
|
|
20
|
+
# @param held [Hash] the `Handover` record: `domain`, and `out` when set
|
|
21
|
+
# @return [Hash{Symbol => Hash}] `output:` one line per file cleared with its settings
|
|
22
|
+
# @raise [ConsoleCapture::Failure] when the domain holds no `.world` file
|
|
23
|
+
def clear_deployment(**held)
|
|
24
|
+
bluebook = File.join(File.expand_path(plain(held[:domain])), "bluebook")
|
|
25
|
+
out = plain(held[:out])
|
|
26
|
+
lines = worlds_of(bluebook).filter_map { |path| clear_world(path, bluebook, out && File.expand_path(out)) }
|
|
27
|
+
{ output: { value: lines.empty? ? "no deployed_to settings to clear" : lines.join("\n") } }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def worlds_of(bluebook)
|
|
33
|
+
worlds = Dir.glob(File.join(bluebook, "**", "*.world"))
|
|
34
|
+
raise ConsoleCapture::Failure, "#{bluebook} holds no .world file to clear" if worlds.empty?
|
|
35
|
+
|
|
36
|
+
worlds
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear_world(path, bluebook, out)
|
|
40
|
+
cleared = WorldClearing.call(File.read(path))
|
|
41
|
+
return unless cleared.changed?
|
|
42
|
+
|
|
43
|
+
target = out ? File.join(out, path.delete_prefix("#{bluebook}/")) : path
|
|
44
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
45
|
+
File.write(target, cleared.text)
|
|
46
|
+
"#{target}: cleared #{cleared.settings.join(", ")}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -10,6 +10,7 @@ require_relative "deploy_plans"
|
|
|
10
10
|
require_relative "deploy_toolchain/statuses"
|
|
11
11
|
require_relative "deploy_toolchain/scripts"
|
|
12
12
|
require_relative "deploy_toolchain/answers"
|
|
13
|
+
require_relative "deploy_toolchain/clearing"
|
|
13
14
|
|
|
14
15
|
module Hecks
|
|
15
16
|
module Adapters
|
|
@@ -26,6 +27,7 @@ module Hecks
|
|
|
26
27
|
include Statuses
|
|
27
28
|
include Scripts
|
|
28
29
|
include Answers
|
|
30
|
+
include Clearing
|
|
29
31
|
|
|
30
32
|
# Accepts the arguments every driven adapter is built with and keeps none of them.
|
|
31
33
|
#
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require_relative "../../../projections/site/role_probe"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
class SiteToolchain
|
|
10
|
+
# Dispatches each role-declaring command to a host with no role and no actor, and reports
|
|
11
|
+
# whether the host refused the caller.
|
|
12
|
+
#
|
|
13
|
+
# A host that does not enforce roles would run the command, so this talks only to a host on
|
|
14
|
+
# this machine: the one a spec or a CI job started for the purpose.
|
|
15
|
+
class RoleProber
|
|
16
|
+
# The addresses that name this machine.
|
|
17
|
+
LOOPBACK = %w[localhost 127.0.0.1 ::1 [::1]].freeze
|
|
18
|
+
|
|
19
|
+
# Seconds to wait to connect to the host, and again for its answer.
|
|
20
|
+
TIMEOUT = 10
|
|
21
|
+
|
|
22
|
+
# The failures of the network that fail one check rather than the whole run.
|
|
23
|
+
NETWORK = [SystemCallError, Timeout::Error, SocketError, JSON::ParserError].freeze
|
|
24
|
+
|
|
25
|
+
# @param checks [Array<Projections::Site::RoleProbe::Check>] the commands to dispatch
|
|
26
|
+
# @param base [String] the host's scheme, host and port
|
|
27
|
+
# @raise [ConsoleCapture::Failure] when the host is not on this machine
|
|
28
|
+
def initialize(checks, base:)
|
|
29
|
+
@checks = checks
|
|
30
|
+
@uri = URI.parse(base)
|
|
31
|
+
return if LOOPBACK.include?(@uri.host)
|
|
32
|
+
|
|
33
|
+
raise ConsoleCapture::Failure, "roles are checked against a host on this machine, not #{@uri.host}: " \
|
|
34
|
+
"a host that does not enforce roles would run each command"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Array<(String, Integer)>] the report, one line per command ending in the count,
|
|
38
|
+
# and how many failed; a command whose synthesized arguments the host refused is counted
|
|
39
|
+
# as unchecked, not failed
|
|
40
|
+
def call
|
|
41
|
+
lines = @checks.map { |check| judged(check) }
|
|
42
|
+
failed = lines.count { |_, state| state == :failed }
|
|
43
|
+
unchecked = lines.count { |_, state| state == :unchecked }
|
|
44
|
+
summary = "#{lines.size} commands, #{failed} failed, #{unchecked} unchecked"
|
|
45
|
+
[lines.map(&:first).append(summary).join("\n"), failed]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def judged(check)
|
|
51
|
+
answer = answer_to(check)
|
|
52
|
+
return [line(check, "unchecked: the host refused the arguments before the role"), :unchecked] if unchecked?(answer)
|
|
53
|
+
|
|
54
|
+
why = Projections::Site::RoleProbe.verdict(check, answer)
|
|
55
|
+
[line(check, why ? "FAILED: #{why}" : "ok"), why ? :failed : :ok]
|
|
56
|
+
rescue *NETWORK => e
|
|
57
|
+
[line(check, "FAILED: #{e.class}: #{e.message}"), :failed]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def unchecked?(answer) = Projections::Site::RoleProbe.unchecked?(answer)
|
|
61
|
+
|
|
62
|
+
def line(check, outcome) = " #{check.verb} (#{check.role}) is refused with no role... #{outcome}"
|
|
63
|
+
|
|
64
|
+
def answer_to(check)
|
|
65
|
+
request = Net::HTTP::Post.new(URI.join(@uri.to_s, "/dispatch"), "Content-Type" => "application/json")
|
|
66
|
+
request.body = JSON.generate(verb: check.verb, with: check.arguments)
|
|
67
|
+
JSON.parse(connection.request(request).body)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def connection
|
|
71
|
+
Net::HTTP.new(@uri.host, @uri.port).tap do |http|
|
|
72
|
+
http.open_timeout = http.read_timeout = TIMEOUT
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -9,6 +9,7 @@ require "hecks/tools"
|
|
|
9
9
|
require "hecks/tools/site_routes"
|
|
10
10
|
require "hecks/projections/site/live_edge"
|
|
11
11
|
require_relative "site_toolchain/prober"
|
|
12
|
+
require_relative "site_toolchain/role_prober"
|
|
12
13
|
|
|
13
14
|
module Hecks
|
|
14
15
|
module Adapters
|
|
@@ -23,7 +24,8 @@ module Hecks
|
|
|
23
24
|
SCRIPTS = { project: "project_site" }.freeze
|
|
24
25
|
|
|
25
26
|
# The tool's flag for each `SiteProjection` field that takes a value.
|
|
26
|
-
FLAGS = { "out" => :out, "template" => :template, "cms" => :cms, "root" => :root, "
|
|
27
|
+
FLAGS = { "out" => :out, "template" => :template, "cms" => :cms, "root" => :root, "editor" => :editor,
|
|
28
|
+
"extension" => :extension }.freeze
|
|
27
29
|
|
|
28
30
|
# Accepts the arguments every driven adapter is built with and keeps none of them.
|
|
29
31
|
#
|
|
@@ -35,7 +37,7 @@ module Hecks
|
|
|
35
37
|
# Writes a project's `routes.ts` from its declared route table, or with `check` only compares.
|
|
36
38
|
#
|
|
37
39
|
# @param held [Hash] the `SiteProjection` record: `domain`, and `out`, `template`, `cms`,
|
|
38
|
-
# `root`, `extension` and `check` when set
|
|
40
|
+
# `root`, `editor`, `extension` and `check` when set
|
|
39
41
|
# @return [Hash{Symbol => Hash}] `output:` one line per file written or found current
|
|
40
42
|
# @raise [ConsoleCapture::Failure] when the route table is refused (an unknown cache class,
|
|
41
43
|
# a duplicate path), a path or the extension is refused, or under `check` a file is out
|
|
@@ -84,6 +86,23 @@ module Hecks
|
|
|
84
86
|
{ output: { value: report } }
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
# Dispatches every command a project declares a role for to a host with no role and no actor,
|
|
90
|
+
# and checks the host refuses each as `Unauthorized`. The host must be on this machine and
|
|
91
|
+
# enforcing roles (`HECKS_ROLE_ENFORCEMENT=enforce`); one that is not would run the commands.
|
|
92
|
+
#
|
|
93
|
+
# @param held [Hash] the `SiteProjection` record: `domain`, the project that declares the
|
|
94
|
+
# commands, and `url`, the host's base address
|
|
95
|
+
# @return [Hash{Symbol => Hash}] `output:` one line per command, ending in the count
|
|
96
|
+
# @raise [ConsoleCapture::Failure] when the host is not on this machine, or a command is not
|
|
97
|
+
# refused for its role; the message is the report
|
|
98
|
+
def roles(**held)
|
|
99
|
+
checks = Projections::Site::RoleProbe.checks(registry_of(located(:domain, held[:domain])))
|
|
100
|
+
report, failed = RoleProber.new(checks, base: base_url(plain(held[:url]))).call
|
|
101
|
+
raise ConsoleCapture::Failure, report if failed.positive?
|
|
102
|
+
|
|
103
|
+
{ output: { value: report } }
|
|
104
|
+
end
|
|
105
|
+
|
|
87
106
|
private
|
|
88
107
|
|
|
89
108
|
# The scheme, host and port of an http(s) address.
|
|
@@ -121,12 +140,18 @@ module Hecks
|
|
|
121
140
|
# The project's checked route table.
|
|
122
141
|
def table_of(root) = read_table(root).last
|
|
123
142
|
|
|
124
|
-
# The registry
|
|
125
|
-
def
|
|
143
|
+
# The registry the project at `root` boots into.
|
|
144
|
+
def registry_of(root)
|
|
126
145
|
registry = nil
|
|
127
146
|
outcome = ConsoleCapture.capture { registry = Tools::SiteRoutes.registry_for(root) }
|
|
128
147
|
raise ConsoleCapture::Failure, outcome.output.strip unless outcome.ok?
|
|
129
148
|
|
|
149
|
+
registry
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The registry, chapter and checked table of the project at `root`.
|
|
153
|
+
def read_table(root)
|
|
154
|
+
registry = registry_of(root)
|
|
130
155
|
chapter = Projections::Site::Table.chapter(registry)
|
|
131
156
|
[registry, chapter, Projections::Site::Table.read(chapter, registry: registry)]
|
|
132
157
|
rescue Projections::Site::Table::Invalid => e
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
# Empties the `deployed_to(...) do ... end` blocks of a `.world` file's text: the region,
|
|
6
|
+
# stacks, secrets, domain and buckets a host deploys with, which a project handed to a client
|
|
7
|
+
# must not carry. Each block keeps its opening and closing lines and holds one comment naming
|
|
8
|
+
# the settings it held, so the file still loads and shows what to set.
|
|
9
|
+
#
|
|
10
|
+
# A block closes at the first `end` at its own indent, and a setting is a line one indent
|
|
11
|
+
# step inside it that starts with its name. A block that holds only comments is already clear
|
|
12
|
+
# and is left as it is, so clearing twice changes nothing the second time.
|
|
13
|
+
class WorldClearing
|
|
14
|
+
BLOCK = /^(?<indent>[ \t]*)deployed_to\b[^\n]*[ \t]do\b[^\n]*\n(?<body>.*?)^\k<indent>end\b[^\n]*\n/m
|
|
15
|
+
|
|
16
|
+
# What clearing one world's text produced.
|
|
17
|
+
#
|
|
18
|
+
# @!attribute [r] text
|
|
19
|
+
# @return [String] the text with every block emptied
|
|
20
|
+
# @!attribute [r] settings
|
|
21
|
+
# @return [Array<String>] the setting names removed, in order, without repeats
|
|
22
|
+
Result = Struct.new(:text, :settings) do
|
|
23
|
+
# @return [Boolean] whether any block was emptied
|
|
24
|
+
def changed? = !settings.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param text [String] the contents of a `.world` file
|
|
28
|
+
# @return [Result] the cleared text and the settings that were in the blocks
|
|
29
|
+
def self.call(text)
|
|
30
|
+
names = []
|
|
31
|
+
cleared = text.gsub(BLOCK) { |block| empty(Regexp.last_match, block, names) }
|
|
32
|
+
Result.new(cleared, names.uniq)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param match [MatchData] one `deployed_to` block
|
|
36
|
+
# @param block [String] the block's text
|
|
37
|
+
# @param names [Array<String>] collects the setting names removed
|
|
38
|
+
# @return [String] the block's replacement
|
|
39
|
+
def self.empty(match, block, names)
|
|
40
|
+
indent = match[:indent]
|
|
41
|
+
held = match[:body].scan(/^#{indent} {2}([a-z_]\w*)/).flatten
|
|
42
|
+
return block if held.empty?
|
|
43
|
+
|
|
44
|
+
names.concat(held)
|
|
45
|
+
"#{block.lines.first}#{comment(indent, note(held.uniq))}#{indent}end\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param held [Array<String>] the setting names the block held
|
|
49
|
+
# @return [String] the comment that stands where the settings were
|
|
50
|
+
def self.note(held)
|
|
51
|
+
"The host's deployment settings were removed from this handoff. Set your own: #{held.join(", ")}."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param indent [String] the block's indent
|
|
55
|
+
# @param note [String] the sentence to put in the block
|
|
56
|
+
# @return [String] the sentence as comment lines inside the block, folded under 100 columns
|
|
57
|
+
def self.comment(indent, note)
|
|
58
|
+
width = 96 - indent.size
|
|
59
|
+
note.scan(/(.{1,#{width}})(?:\s+|\z)/).flatten.map { |line| "#{indent} # #{line}\n" }.join
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private_class_method :empty, :note, :comment
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -407,6 +407,14 @@ Hecks.hecksagon "Hecks" do
|
|
|
407
407
|
end
|
|
408
408
|
end
|
|
409
409
|
|
|
410
|
+
Deploy::Handover.port "DeployToolchain" do
|
|
411
|
+
asks "ClearDeployment", to: Handover do
|
|
412
|
+
attribute :run, RunKey
|
|
413
|
+
answers "HandoverAnswered"
|
|
414
|
+
refuses "HandoverRefused"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
410
418
|
Deploy::MakefileCheck.port "DeployToolchain" do
|
|
411
419
|
asks "Scan", to: MakefileCheck do
|
|
412
420
|
attribute :run, RunKey
|
data/lib/hecks/hecks/hecks.world
CHANGED
|
@@ -47,7 +47,8 @@ Hecks.world "Hecks" do
|
|
|
47
47
|
service_roll.run box_roll.run data_copy.restore
|
|
48
48
|
data_copy.verify bluebook_diff.run preview_run.name
|
|
49
49
|
preview_run.url preview_run.list preview_run.deploy
|
|
50
|
-
preview_run.destroy preview_run.login companion_roll.run
|
|
50
|
+
preview_run.destroy preview_run.login companion_roll.run
|
|
51
|
+
handover.clear],
|
|
51
52
|
names: { "mcp" => "door.serve_mcp", "console" => "operation.open_console", "init" => "door.init",
|
|
52
53
|
"interview" => "door.interview", "package.check" => "registry.check",
|
|
53
54
|
"package.digest" => "registry.digest",
|
|
@@ -38,6 +38,7 @@ module Hecks
|
|
|
38
38
|
plan.domain_env = Check.map!(settings.fetch(:domain_env, {}), "domain_env", nil_ok: true)
|
|
39
39
|
plan.install_dir = install_dir(settings.fetch(:install_dir, DEFAULT_INSTALL_DIR))
|
|
40
40
|
plan.build_context_dir = build_context_dir(settings.fetch(:build_context_dir, ""))
|
|
41
|
+
plan.host_dir = host_dir(settings.fetch(:host_crate, DEFAULT_HOST_DIR))
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def check_execution_grant!(plan)
|
|
@@ -62,6 +63,20 @@ module Hecks
|
|
|
62
63
|
raise ArgumentError, "install_dir must be an absolute path without a trailing slash, got #{value.inspect}"
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# The directory of the Cargo package the Makefile builds `bootstrap` from: an absolute
|
|
67
|
+
# path, or one relative to the Makefile's `ROOT`. Only path characters are accepted,
|
|
68
|
+
# since the value is written into a Makefile that shell lines expand.
|
|
69
|
+
def host_dir(value)
|
|
70
|
+
text = value.to_s
|
|
71
|
+
return text if text == DEFAULT_HOST_DIR
|
|
72
|
+
return text if text.match?(%r{\A/[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*\z})
|
|
73
|
+
return "$(ROOT)/#{text}" if text.match?(%r{\A[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*\z})
|
|
74
|
+
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"host_crate must be a directory path of letters, digits, . _ - and /, absolute or relative to the " \
|
|
77
|
+
"Makefile's ROOT, got #{value.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
65
80
|
def build_context_dir(value)
|
|
66
81
|
text = value.to_s
|
|
67
82
|
return text if text.empty? || text.match?(%r{\A[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*/\z})
|
|
@@ -19,7 +19,7 @@ module Hecks
|
|
|
19
19
|
Plan = Struct.new(
|
|
20
20
|
:ids, :names, :layout, :cdn, :alerts, :extras, :execute_command, :health_check_grace_period,
|
|
21
21
|
:deregistration_delay, :desired_count_parameter, :domain_env, :install_dir, :build_context_dir,
|
|
22
|
-
:db_name_parameter, :execution_database_grant,
|
|
22
|
+
:db_name_parameter, :execution_database_grant, :host_dir,
|
|
23
23
|
keyword_init: true
|
|
24
24
|
)
|
|
25
25
|
|
|
@@ -37,6 +37,8 @@ module Hecks
|
|
|
37
37
|
}.freeze
|
|
38
38
|
NAME_ROLES = [:cluster, :log_group, :service, :alb, :family, :alb_security_group_description, :db_secret_policy].freeze
|
|
39
39
|
DEFAULT_INSTALL_DIR = "/usr/local/bin".freeze
|
|
40
|
+
# Where the generated Makefile builds the host binary unless `host_crate` names another.
|
|
41
|
+
DEFAULT_HOST_DIR = "$(ROOT)/rust/host".freeze
|
|
40
42
|
|
|
41
43
|
extend Ids
|
|
42
44
|
extend Tuning
|
|
@@ -65,6 +65,7 @@ module Hecks
|
|
|
65
65
|
def vpc_ref = shared ? "!Ref OwningVpcId" : "!Ref #{db_id}Vpc"
|
|
66
66
|
def sidecar_dir = plan.install_dir
|
|
67
67
|
def build_context_dir = plan.build_context_dir
|
|
68
|
+
def host_dir = plan.host_dir
|
|
68
69
|
def cmd_binary = sidecar_dir == Settings::DEFAULT_INSTALL_DIR ? domain_name : "#{sidecar_dir}/#{domain_name}"
|
|
69
70
|
def desired_count_value = plan.desired_count_parameter ? "!Ref #{plan.desired_count_parameter}" : desired_count
|
|
70
71
|
def declared_domain_literal = declared_domain_name.inspect
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# GENERATED by hecks deploy project @@domain@@ — re-run it to refresh
|
|
2
2
|
# this file rather than hand-editing.
|
|
3
3
|
#
|
|
4
|
-
# `make deploy` — builds
|
|
5
|
-
#
|
|
4
|
+
# `make deploy` — builds the host for this domain (rust/host, or the
|
|
5
|
+
# directory `host_crate` names), pushes it to this stack's own ECR
|
|
6
|
+
# repository, and deploys the CloudFormation stack.
|
|
6
7
|
# No SAM anywhere in this path.
|
|
7
8
|
|
|
8
9
|
ROOT := @@root@@
|
|
@@ -11,6 +12,8 @@ STACK := @@stack_name@@
|
|
|
11
12
|
BASTION_STACK := @@stack_name@@-bastion
|
|
12
13
|
REGION := @@region@@
|
|
13
14
|
IMAGE_TAG := latest
|
|
15
|
+
# The Cargo package `bootstrap` is built from; `make HOST_DIR=<dir>` builds another.
|
|
16
|
+
HOST_DIR ?= @@host_dir@@
|
|
14
17
|
|
|
15
18
|
build:
|
|
16
19
|
# aarch64, not x86_64 — matches template.yaml's own
|
|
@@ -33,8 +36,8 @@ build:
|
|
|
33
36
|
# rust/dist/@@domain_name@@.wasm/.ir.json from this domain's own
|
|
34
37
|
# .bluebook.
|
|
35
38
|
cd $(ROOT) && HECKS_ENVIRONMENT=memory ruby exe/hecks build.build_wasm domain=$(DOMAIN) --wait
|
|
36
|
-
cd $(
|
|
37
|
-
cp $(
|
|
39
|
+
cd $(HOST_DIR) && CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc rustup run stable cargo build --release --target aarch64-unknown-linux-gnu --bin bootstrap
|
|
40
|
+
cp $(HOST_DIR)/target/aarch64-unknown-linux-gnu/release/bootstrap @@domain_name@@-host
|
|
38
41
|
cp $(ROOT)/rust/dist/@@domain_name@@.wasm @@domain_name@@.wasm
|
|
39
42
|
cp $(ROOT)/rust/dist/@@domain_name@@.ir.json @@domain_name@@.ir.json
|
|
40
43
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
module Site
|
|
6
|
+
module CmsEditor
|
|
7
|
+
# One attribute as the editor reads it: its name, the type as the bluebook spells it, how a
|
|
8
|
+
# value of it is entered (`kind`), and whether it is optional or a list.
|
|
9
|
+
#
|
|
10
|
+
# A type that names one of the aggregate's value objects is an `object`; a reference is a
|
|
11
|
+
# `reference`; the primitives are `integer`, `number`, `boolean` and `text`, and any other
|
|
12
|
+
# name is entered as text.
|
|
13
|
+
#
|
|
14
|
+
# An attribute whose value object has the shape of a structured document, a `blocks`
|
|
15
|
+
# list of a value object that has `kind` and `spans`, carries `widget: "body"` and is
|
|
16
|
+
# edited with the rich-text widget. The shape decides it, never a name.
|
|
17
|
+
class Attributes
|
|
18
|
+
# The parts a value object needs for its list of blocks to be a document body's blocks.
|
|
19
|
+
BLOCK_PARTS = %w[kind spans].freeze
|
|
20
|
+
|
|
21
|
+
# The kind a primitive's name enters as; any other name is text.
|
|
22
|
+
KINDS = { "Integer" => "integer", "Float" => "number", "Boolean" => "boolean", "TrueClass" => "boolean",
|
|
23
|
+
"FalseClass" => "boolean" }.freeze
|
|
24
|
+
|
|
25
|
+
# @param objects [Array<Bluebook::ValueObject>] the aggregate's value objects
|
|
26
|
+
def initialize(objects)
|
|
27
|
+
@by_name = objects.to_h { |object| [object.hecks_name, object] }
|
|
28
|
+
@objects = @by_name.keys
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param attribute [Bluebook::Attribute] a declared attribute
|
|
32
|
+
# @return [Hash{String => Object}] the attribute as the editor reads it
|
|
33
|
+
def of(attribute)
|
|
34
|
+
type = attribute.type
|
|
35
|
+
return reference(attribute, type.target_name) if type.is_a?(Bluebook::Reference)
|
|
36
|
+
|
|
37
|
+
entry = base(attribute, type.to_s, kind(type.to_s))
|
|
38
|
+
body?(attribute) ? entry.merge("widget" => "body") : entry
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# A single (not listed) attribute whose value object holds a list of document blocks.
|
|
44
|
+
def body?(attribute)
|
|
45
|
+
return false if attribute.list?
|
|
46
|
+
|
|
47
|
+
block = block_object(@by_name[attribute.type.to_s])
|
|
48
|
+
!block.nil? && (BLOCK_PARTS - block.attributes.map { |part| part.name.to_s }).empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Bluebook::ValueObject, nil] the value object `object`'s `blocks` list holds
|
|
52
|
+
def block_object(object)
|
|
53
|
+
blocks = object&.attributes&.find { |part| part.name.to_s == "blocks" && part.list? }
|
|
54
|
+
blocks && @by_name[blocks.type.to_s]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reference(attribute, target)
|
|
58
|
+
base(attribute, target, "reference").merge("target" => target)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def base(attribute, type, kind)
|
|
62
|
+
{ "name" => attribute.name.to_s, "type" => type, "kind" => kind,
|
|
63
|
+
"optional" => attribute.optional?, "list" => attribute.list? }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def kind(type)
|
|
67
|
+
return "object" if @objects.include?(type)
|
|
68
|
+
|
|
69
|
+
KINDS.fetch(type, "text")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|