hecks 3.1.3 → 3.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/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
- data/lib/hecks/deploy/oidc.json +15 -0
- data/lib/hecks/hecks/adapters/cost_explorer.adapter +7 -0
- data/lib/hecks/hecks/adapters/cost_explorer.rb +102 -0
- data/lib/hecks/hecks/hecks.hecksagon +8 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/auth.rs +25 -1
- data/rust/host/src/web.rs +9 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c79ef6cb53e23b9faa0f452f116af208c659512f00716e47c7f3cdaad3d51e86
|
|
4
|
+
data.tar.gz: b693ea21f847607ca2964759f9e0cec9dc7c4f8e1ea341b6636e99f8b4b1d294
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38d1467d95484bcf093d6671982bf5e4edc4fc1be8907d849a88df9b85ab2b9810f88ce9533a9d1d125187fd47c26191fbe3e104e90307ee6fb19b3cffa37546
|
|
7
|
+
data.tar.gz: 329631cf0d425b281c4a4770318d2956ae95ca7d2e0b12e2e0054bd2f0d72a3310a92d1ce2fa125d5a056c76026c27472230a0710335790267db173ada8e48db
|
|
@@ -665,6 +665,111 @@ Hecks.bluebook "Deploy" do
|
|
|
665
665
|
trigger MakefileCheck::Flag, with: { refusal: :refusal }
|
|
666
666
|
end
|
|
667
667
|
|
|
668
|
+
aggregate "CostCheck" do
|
|
669
|
+
description "One reading of what hosting costs a month, against a budget."
|
|
670
|
+
|
|
671
|
+
attribute :run, RunKey
|
|
672
|
+
attribute :budget, Budget
|
|
673
|
+
attribute :since, Since
|
|
674
|
+
attribute :report, Report, optional: true
|
|
675
|
+
attribute :refusal, Report, optional: true
|
|
676
|
+
identified_by :run
|
|
677
|
+
|
|
678
|
+
value_object "RunKey" do
|
|
679
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
680
|
+
|
|
681
|
+
invariant("a run is named") { !value.to_s.empty? }
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Whole dollars a month.
|
|
685
|
+
value_object "Budget" do
|
|
686
|
+
attribute :value, Integer
|
|
687
|
+
|
|
688
|
+
invariant("a budget is positive") { value.positive? }
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# The first day to count, as `YYYY-MM-DD`: the first complete day after the last change to
|
|
692
|
+
# what is running, so a bill that still carries the old setup is not read as the new one.
|
|
693
|
+
value_object "Since" do
|
|
694
|
+
attribute :value, String, pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}'
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
value_object "Report" do
|
|
698
|
+
attribute :value, String
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
lifecycle :status, default: "requested" do
|
|
702
|
+
transition "Pass" => "within_budget", from: "requested"
|
|
703
|
+
transition "Flag" => "flagged", from: "requested"
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# The reading is the billed daily total from the first complete day since `since` up to
|
|
707
|
+
# yesterday, scaled to a month; today is left out because its bill is still partial.
|
|
708
|
+
command "Check" do
|
|
709
|
+
goal "Read what hosting has billed since a day and compare its monthly rate to a budget"
|
|
710
|
+
|
|
711
|
+
attribute :run, RunKey
|
|
712
|
+
attribute :budget, Budget
|
|
713
|
+
attribute :since, Since
|
|
714
|
+
|
|
715
|
+
sets :run
|
|
716
|
+
sets :budget
|
|
717
|
+
sets :since
|
|
718
|
+
|
|
719
|
+
emits "CostCheckRequested"
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
command "Pass" do
|
|
723
|
+
goal "Record that the monthly rate is within the budget, and what it was"
|
|
724
|
+
|
|
725
|
+
reference_to CostCheck
|
|
726
|
+
attribute :report, Report
|
|
727
|
+
|
|
728
|
+
sets :report
|
|
729
|
+
|
|
730
|
+
emits "CostWithinBudget"
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
command "Flag" do
|
|
734
|
+
goal "Record that the monthly rate is over the budget, or that it could not be read, and what was said"
|
|
735
|
+
|
|
736
|
+
reference_to CostCheck
|
|
737
|
+
attribute :refusal, Report
|
|
738
|
+
|
|
739
|
+
sets :refusal
|
|
740
|
+
|
|
741
|
+
emits "CostFlagged"
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
query "Verdict" do
|
|
745
|
+
description "What one reading found: its status, and the report or the reason it was flagged."
|
|
746
|
+
attribute :run, RunKey
|
|
747
|
+
where(run: :run)
|
|
748
|
+
order_by :run
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
query "Flagged" do
|
|
752
|
+
description "Every reading that was over its budget or could not be taken."
|
|
753
|
+
where(status: "flagged")
|
|
754
|
+
order_by :run
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
policy "MeasureWhenChecked" do
|
|
759
|
+
on "CostCheck.CostCheckRequested"
|
|
760
|
+
trigger CostCheck::CostExplorer::Measure, with: { run: :run }
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
policy "RecordTheCostWithinBudget" do
|
|
764
|
+
on "CostCheck.CostAnswered"
|
|
765
|
+
trigger CostCheck::Pass, with: { report: :report }
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
policy "RecordTheFlaggedCost" do
|
|
769
|
+
on "CostCheck.CostRefused"
|
|
770
|
+
trigger CostCheck::Flag, with: { refusal: :refusal }
|
|
771
|
+
end
|
|
772
|
+
|
|
668
773
|
aggregate "TemplateComparison" do
|
|
669
774
|
description "One offline comparison of two CloudFormation templates, by logical id."
|
|
670
775
|
|
|
@@ -35,6 +35,14 @@ Hecks.hecksagon "Deploy" do
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
Deploy::CostCheck.port "CostExplorer" do
|
|
39
|
+
asks "Measure", to: CostCheck do
|
|
40
|
+
attribute :run, RunKey
|
|
41
|
+
answers "CostAnswered"
|
|
42
|
+
refuses "CostRefused"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
38
46
|
Deploy::TemplateComparison.port "DeployToolchain" do
|
|
39
47
|
asks "Compare", to: TemplateComparison do
|
|
40
48
|
attribute :run, RunKey
|
data/lib/hecks/deploy/oidc.json
CHANGED
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
"verb": "Deploy::BoxTarget.Declare",
|
|
7
7
|
"role": null
|
|
8
8
|
},
|
|
9
|
+
{
|
|
10
|
+
"scope": "deploy:cost_check.check",
|
|
11
|
+
"verb": "Deploy::CostCheck.Check",
|
|
12
|
+
"role": null
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"scope": "deploy:cost_check.flag",
|
|
16
|
+
"verb": "Deploy::CostCheck.Flag",
|
|
17
|
+
"role": null
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"scope": "deploy:cost_check.pass",
|
|
21
|
+
"verb": "Deploy::CostCheck.Pass",
|
|
22
|
+
"role": null
|
|
23
|
+
},
|
|
9
24
|
{
|
|
10
25
|
"scope": "deploy:fargate_target.declare",
|
|
11
26
|
"verb": "Deploy::FargateTarget.Declare",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
|
|
2
|
+
# and the implementation must load once, from `lib/`, not from the copy.
|
|
3
|
+
require "hecks/hecks/adapters/cost_explorer"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "CostExplorer" do
|
|
6
|
+
port "CostExplorer"
|
|
7
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
require "date"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The `CostExplorer` port's adapter: reads what an AWS account has billed, with the `aws`
|
|
10
|
+
# command, and compares the monthly rate to a budget.
|
|
11
|
+
#
|
|
12
|
+
# It reads the daily unblended cost from the check's `since` day up to yesterday. Today is left
|
|
13
|
+
# out because its bill is still partial, and a day is counted whatever Cost Explorer marks it
|
|
14
|
+
# (a day stays "estimated" for a while after it ends). The rate is the mean daily cost scaled
|
|
15
|
+
# to an average month. A rate within the budget is answered with a one-line report; a rate over
|
|
16
|
+
# it, a span with no complete day, or a failed `aws` call is refused with a reason, which the
|
|
17
|
+
# runtime records on the check.
|
|
18
|
+
class CostExplorer
|
|
19
|
+
# Days in an average month, for scaling a daily mean to a monthly rate.
|
|
20
|
+
DAYS_IN_MONTH = 30.4375
|
|
21
|
+
|
|
22
|
+
# How many of the biggest services the report names.
|
|
23
|
+
TOP_SERVICES = 3
|
|
24
|
+
|
|
25
|
+
# @param aggregate [Object, nil] unused
|
|
26
|
+
# @param settings [Hash] `today:` pins the day the span ends before, for tests
|
|
27
|
+
# @param root [String, nil] unused
|
|
28
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
29
|
+
@today = settings[:today]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Reads the billed cost since a day and compares its monthly rate to the budget.
|
|
33
|
+
#
|
|
34
|
+
# @param held [Hash] the check's fields: `budget:` whole dollars a month and `since:` the
|
|
35
|
+
# first day to count (`YYYY-MM-DD`), each a value object or a plain value
|
|
36
|
+
# @return [Hash{Symbol => Hash}] `report:` the rate against the budget, the shape
|
|
37
|
+
# `CostCheck.Pass` takes
|
|
38
|
+
# @raise [RuntimeError] when no complete day has passed since `since`, `aws` fails, or the
|
|
39
|
+
# rate is over the budget
|
|
40
|
+
def measure(**held)
|
|
41
|
+
budget = Integer(plain(held[:budget]))
|
|
42
|
+
first = Date.iso8601(plain(held[:since]).to_s)
|
|
43
|
+
last = today
|
|
44
|
+
raise "no complete day since #{first}; today (#{last}) is still being billed" if first >= last
|
|
45
|
+
|
|
46
|
+
days = daily_costs(first, last)
|
|
47
|
+
line = report_line(first, last, days, budget)
|
|
48
|
+
raise "over budget: #{line}" if monthly_rate(days) > budget
|
|
49
|
+
|
|
50
|
+
{ report: { value: line } }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# @return [Date] the day the span ends before: today, in UTC
|
|
56
|
+
def today = @today ? Date.iso8601(@today.to_s) : Time.now.utc.to_date
|
|
57
|
+
|
|
58
|
+
# @return [Hash{String => Hash{String => Float}}] day => service => dollars
|
|
59
|
+
def daily_costs(first, last)
|
|
60
|
+
out, err, status = Open3.capture3(
|
|
61
|
+
"aws", "ce", "get-cost-and-usage", "--time-period", "Start=#{first},End=#{last}",
|
|
62
|
+
"--granularity", "DAILY", "--metrics", "UnblendedCost",
|
|
63
|
+
"--group-by", "Type=DIMENSION,Key=SERVICE", "--output", "json"
|
|
64
|
+
)
|
|
65
|
+
raise "aws ce get-cost-and-usage failed: #{err.strip.empty? ? out.strip : err.strip}" unless status.success?
|
|
66
|
+
|
|
67
|
+
rows = JSON.parse(out).fetch("ResultsByTime")
|
|
68
|
+
raise "Cost Explorer returned no days for #{first}..#{last}" if rows.empty?
|
|
69
|
+
|
|
70
|
+
rows.to_h { |row| [row.dig("TimePeriod", "Start"), by_service(row)] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def by_service(row)
|
|
74
|
+
row.fetch("Groups").to_h do |group|
|
|
75
|
+
[group.fetch("Keys").first, group.dig("Metrics", "UnblendedCost", "Amount").to_f]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Float] the mean daily total, scaled to an average month
|
|
80
|
+
def monthly_rate(days)
|
|
81
|
+
mean = days.values.sum { |services| services.values.sum } / days.size
|
|
82
|
+
mean * DAYS_IN_MONTH
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def report_line(first, last, days, budget)
|
|
86
|
+
mean = days.values.sum { |services| services.values.sum } / days.size
|
|
87
|
+
biggest = totals_by_service(days).max_by(TOP_SERVICES) { |_name, dollars| dollars }
|
|
88
|
+
named = biggest.map { |name, dollars| "#{name} $#{format('%.2f', dollars / days.size)}/day" }.join(", ")
|
|
89
|
+
"#{first}..#{last - 1} (#{days.size} days): $#{format('%.2f', mean)}/day, " \
|
|
90
|
+
"$#{format('%.2f', monthly_rate(days))}/month against $#{budget}; biggest: #{named}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def totals_by_service(days)
|
|
94
|
+
days.values.each_with_object(Hash.new(0.0)) do |services, totals|
|
|
95
|
+
services.each { |name, dollars| totals[name] += dollars }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -368,6 +368,14 @@ Hecks.hecksagon "Hecks" do
|
|
|
368
368
|
end
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
+
Deploy::CostCheck.port "CostExplorer" do
|
|
372
|
+
asks "Measure", to: CostCheck do
|
|
373
|
+
attribute :run, RunKey
|
|
374
|
+
answers "CostAnswered"
|
|
375
|
+
refuses "CostRefused"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
371
379
|
Deploy::TemplateComparison.port "DeployToolchain" do
|
|
372
380
|
asks "Compare", to: TemplateComparison do
|
|
373
381
|
attribute :run, RunKey
|
|
@@ -62,7 +62,7 @@ ROLL=$(jq -n --arg compose "$(B64 "$WORK/compose.json")" --arg secrets "$(B64 "$
|
|
|
62
62
|
"aws ecr get-login-password --region '"$REGION"' | docker login --username AWS --password-stdin \($registry) >/dev/null",
|
|
63
63
|
"docker compose -f compose.json pull --quiet",
|
|
64
64
|
"docker compose -f compose.json up -d --remove-orphans",
|
|
65
|
-
"sleep
|
|
65
|
+
"for i in $(seq 1 40); do docker compose -f compose.json ps --format \"{{.Status}}\" | grep -qE \"^Up (Less than a second|[0-4] seconds?)|Restarting|Created|Exited\" || break; sleep 1; done; docker compose -f compose.json ps --format \"table {{.Service}}\\t{{.Status}}\""],
|
|
66
66
|
executionTimeout: ["900"]}')
|
|
67
67
|
|
|
68
68
|
echo "==> rolling $BOX_STACK ($BOX) at $(date -u +%H:%M:%SZ)"
|
|
@@ -252,6 +252,7 @@ module Hecks
|
|
|
252
252
|
# @return [void]
|
|
253
253
|
def write(artifact, out_dir)
|
|
254
254
|
written = Hecks::Projector.write(artifact, out_dir, as: :files)
|
|
255
|
+
written.each { |path| File.chmod(0o755, path) if path.end_with?(".sh") }
|
|
255
256
|
|
|
256
257
|
written.each { |path| puts "wrote #{path}" }
|
|
257
258
|
puts
|
data/lib/hecks/version.rb
CHANGED
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.2.0
|
data/rust/host/src/auth.rs
CHANGED
|
@@ -592,8 +592,25 @@ pub async fn active_role(client: &Mutex<Client>, domain_ir: &Value, email: &str)
|
|
|
592
592
|
.and_then(|(_, state)| role_of(state).map(String::from)))
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
/// The Governance identity id linked to the member with `email`, read from
|
|
596
|
+
/// the live membership head. `None` when the member has not signed in yet.
|
|
597
|
+
pub async fn member_identity_id(client: &Mutex<Client>, domain_ir: &Value, email: &str) -> anyhow::Result<Option<String>> {
|
|
598
|
+
let email = email.to_lowercase();
|
|
599
|
+
Ok(member_rows(client, domain_ir)
|
|
600
|
+
.await?
|
|
601
|
+
.iter()
|
|
602
|
+
.find(|(id, state)| id.to_lowercase() == email && !is_deleted(state))
|
|
603
|
+
.and_then(|(_, state)| linked_identity_id(state)))
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// The id the sign-in flow wrote onto a Member row, or `None` before then.
|
|
607
|
+
fn linked_identity_id(state: &Value) -> Option<String> {
|
|
608
|
+
state.get("identity_id").and_then(|v| v.get("value")).and_then(|v| v.as_str()).map(str::to_string)
|
|
609
|
+
}
|
|
610
|
+
|
|
595
611
|
/// Every admitted person as a JSON row: name, email, role, `linked`
|
|
596
|
-
/// (signed in), `granted` (has access), and `
|
|
612
|
+
/// (signed in), `granted` (has access), `disabled`, and `identity_id`
|
|
613
|
+
/// (the Governance identity id, `null` until the person has signed in).
|
|
597
614
|
pub async fn all_people(client: &Mutex<Client>, domain_ir: &Value) -> anyhow::Result<Vec<Value>> {
|
|
598
615
|
Ok(member_rows(client, domain_ir)
|
|
599
616
|
.await?
|
|
@@ -610,6 +627,7 @@ pub async fn all_people(client: &Mutex<Client>, domain_ir: &Value) -> anyhow::Re
|
|
|
610
627
|
"linked": linked,
|
|
611
628
|
"granted": role.is_some() && !disabled,
|
|
612
629
|
"disabled": disabled,
|
|
630
|
+
"identity_id": linked_identity_id(&state),
|
|
613
631
|
})
|
|
614
632
|
})
|
|
615
633
|
.collect())
|
|
@@ -1206,9 +1224,15 @@ mod tests {
|
|
|
1206
1224
|
let chris = people.iter().find(|p| p["email"] == "chris@example.com").unwrap();
|
|
1207
1225
|
assert_eq!(chris["linked"], true);
|
|
1208
1226
|
assert_eq!(chris["granted"], true);
|
|
1227
|
+
assert_eq!(chris["identity_id"], "id-1");
|
|
1209
1228
|
let angie = people.iter().find(|p| p["email"] == "angie@example.com").unwrap();
|
|
1210
1229
|
assert_eq!(angie["linked"], false);
|
|
1211
1230
|
assert_eq!(angie["granted"], false);
|
|
1231
|
+
assert!(angie["identity_id"].is_null());
|
|
1232
|
+
|
|
1233
|
+
assert_eq!(member_identity_id(&db, &domain_ir, "Chris@Example.com").await.unwrap().as_deref(), Some("id-1"));
|
|
1234
|
+
assert_eq!(member_identity_id(&db, &domain_ir, "angie@example.com").await.unwrap(), None);
|
|
1235
|
+
assert_eq!(member_identity_id(&db, &domain_ir, "nobody@example.com").await.unwrap(), None);
|
|
1212
1236
|
}
|
|
1213
1237
|
|
|
1214
1238
|
#[tokio::test]
|
data/rust/host/src/web.rs
CHANGED
|
@@ -369,7 +369,10 @@ async fn active_session_email(
|
|
|
369
369
|
|
|
370
370
|
async fn accounts_me_route(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &str, client: &Mutex<Client>) -> Value {
|
|
371
371
|
match active_session_email(domain_ir, cookies, secret, client).await {
|
|
372
|
-
Ok(email) =>
|
|
372
|
+
Ok(email) => match auth::member_identity_id(client, domain_ir, &email).await {
|
|
373
|
+
Ok(identity_id) => respond(200, "application/json", &json!({"email": email, "identity_id": identity_id}).to_string()),
|
|
374
|
+
Err(e) => respond(500, "application/json", &json!({"error": format!("members lookup failed: {e}")}).to_string()),
|
|
375
|
+
},
|
|
373
376
|
Err(response) => response,
|
|
374
377
|
}
|
|
375
378
|
}
|
|
@@ -434,7 +437,7 @@ pub(crate) async fn add_member_route(
|
|
|
434
437
|
Ok(true) => respond(
|
|
435
438
|
201,
|
|
436
439
|
"application/json",
|
|
437
|
-
&json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false}).to_string(),
|
|
440
|
+
&json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false, "identity_id": null}).to_string(),
|
|
438
441
|
),
|
|
439
442
|
Ok(false) => json_error(500, &format!("the person was admitted but the {role} role was not granted")),
|
|
440
443
|
Err(e) => json_error(500, &format!("the person was admitted but the {role} role was not granted: {e}")),
|
|
@@ -2173,6 +2176,7 @@ mod tests {
|
|
|
2173
2176
|
assert_eq!(response["statusCode"], 200, "{response:?}");
|
|
2174
2177
|
let body: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
|
|
2175
2178
|
assert_eq!(body["email"], "zed@example.com");
|
|
2179
|
+
assert_eq!(body["identity_id"], "id-1", "the signed-in account's Governance identity id");
|
|
2176
2180
|
|
|
2177
2181
|
let empty = HashMap::new();
|
|
2178
2182
|
assert_eq!(accounts_me_route(&domain_ir, &empty, secret, &client).await["statusCode"], 401);
|
|
@@ -2273,11 +2277,11 @@ mod tests {
|
|
|
2273
2277
|
assert_eq!(names, ["amy", "Zed"], "sorted case-insensitively by name");
|
|
2274
2278
|
assert_eq!(
|
|
2275
2279
|
people[1],
|
|
2276
|
-
json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false})
|
|
2280
|
+
json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false, "identity_id": "id-1"})
|
|
2277
2281
|
);
|
|
2278
2282
|
assert_eq!(
|
|
2279
2283
|
people[0],
|
|
2280
|
-
json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false})
|
|
2284
|
+
json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false, "identity_id": null})
|
|
2281
2285
|
);
|
|
2282
2286
|
}
|
|
2283
2287
|
|
|
@@ -2465,7 +2469,7 @@ mod tests {
|
|
|
2465
2469
|
let created: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
|
|
2466
2470
|
assert_eq!(
|
|
2467
2471
|
created,
|
|
2468
|
-
json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false})
|
|
2472
|
+
json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false, "identity_id": null})
|
|
2469
2473
|
);
|
|
2470
2474
|
|
|
2471
2475
|
let listing = members_route(&domain_ir, &cookies, secret, &client).await;
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
@@ -380,6 +380,8 @@ files:
|
|
|
380
380
|
- lib/hecks/hecks/adapters/codebase/test_suite.rb
|
|
381
381
|
- lib/hecks/hecks/adapters/codebase/tree.rb
|
|
382
382
|
- lib/hecks/hecks/adapters/console_capture.rb
|
|
383
|
+
- lib/hecks/hecks/adapters/cost_explorer.adapter
|
|
384
|
+
- lib/hecks/hecks/adapters/cost_explorer.rb
|
|
383
385
|
- lib/hecks/hecks/adapters/deploy_toolchain.adapter
|
|
384
386
|
- lib/hecks/hecks/adapters/deploy_toolchain.rb
|
|
385
387
|
- lib/hecks/hecks/adapters/finding_github.adapter
|