cli/vazure.bend

near-v-framework@0.2.0.0 · 203 lines · 0 definitions

1import Base
2import near-v-framework@0.2.0.0/lib/effs/io.bend as X
3import near-v-framework@0.2.0.0/lib/vstr.bend as S
4import near-v-framework@0.2.0.0/cli/project.bend as P
5import bend-net-json@0.3.0.0/json.bend as Json
6
7# Only ARM/DevOps read commands with narrow JMESPath projections appear here.
8# Disable Azure CLI command logs: even metadata must not create files outside the project.
9def parsed(m: Maybe<&2, Json.Val>, label: String) -> IO(Json.Val):
10  match m:
11    case None{}: IO.die(Json.Val, 1, "snapshot: " ++ label ++ " returned invalid JSON")
12    case Some{value}: IO.pure(Json.Val, value)
13
14def result_json(result: Result<&1, &1, U32 & String, String>, label: String) -> IO(Json.Val):
15  match result:
16    case Fail{_}: IO.die(Json.Val, 1, "snapshot: " ++ label ++ " read failed")
17    case Done{raw}: parsed(Json.parse(raw), label)
18
19def read_json(label: String, command: String) -> IO(Json.Val):
20  do IO<Json.Val>:
21    result : Result<&1, &1, U32 & String, String> <- X.Proc.run("AZURE_LOGGING_ENABLE_LOG_FILE=false " ++ command ++ " 2>/dev/null")
22    result_json(result, label)
23
24def json_or_null(m: Maybe<&2, Json.Val>) -> Json.Val:
25  match m:
26    case None{}: Json.Null{}
27    case Some{v}: v
28
29def at(v: Json.Val, k: String) -> Json.Val:
30  json_or_null(Json.get(v, k))
31
32def string(v: Json.Val) -> String:
33  match v:
34    case Json.Str{s}: s
35    case Json.Num{s}: s
36    case _: ""
37
38def array(v: Json.Val) -> List<&2, Json.Val>:
39  match v:
40    case Json.Arr{xs}: xs
41    case _: []
42
43def lit(s: String) -> String:
44  "\"" ++ S.esc(s) ++ "\""
45
46def quoted(xs: List<&2, String>) -> List<&2, String>:
47  match xs:
48    case []: []
49    case x <> rest: lit(x) <> quoted(rest)
50
51def strings(xs: List<&2, Json.Val>) -> List<&2, String>:
52  match xs:
53    case []: []
54    case x <> rest: string(x) <> strings(rest)
55
56def str_list(v: Json.Val) -> String:
57  "[" ++ String.join(quoted(strings(array(v))), ", ") ++ "]"
58
59def resource(+v: Json.Val) -> String:
60  "A.Resource{" ++ lit(string(at(v, "name"))) ++ ", " ++ lit(string(at(v, "type"))) ++ ", " ++ lit(string(at(v, "resourceGroup"))) ++ ", " ++ lit(string(at(v, "location"))) ++ "}"
61
62def project(v: Json.Val) -> String:
63  "A.Project{" ++ lit(string(at(v, "name"))) ++ "}"
64
65def repository(v: Json.Val) -> String:
66  "A.Repository{" ++ lit(string(at(v, "name"))) ++ "}"
67
68def pipeline(+v: Json.Val) -> String:
69  "A.Pipeline{" ++ lit(string(at(v, "name"))) ++ ", " ++ lit(string(at(v, "repository"))) ++ ", " ++ lit(string(at(v, "yaml"))) ++ "}"
70
71def connection(+v: Json.Val) -> String:
72  "A.ServiceConnection{" ++ lit(string(at(v, "name"))) ++ ", " ++ lit(string(at(v, "type"))) ++ ", " ++ lit(string(at(v, "scheme"))) ++ "}"
73
74def resource_items(xs: List<&2, Json.Val>) -> List<&2, String>:
75  match xs:
76    case []: []
77    case x <> rest: resource(x) <> resource_items(rest)
78
79def group_names(xs: List<&2, Json.Val>) -> List<&2, String>:
80  match xs:
81    case []: []
82    case x <> rest: string(at(x, "name")) <> group_names(rest)
83
84def project_items(xs: List<&2, Json.Val>) -> List<&2, String>:
85  match xs:
86    case []: []
87    case x <> rest: project(x) <> project_items(rest)
88
89def repository_items(xs: List<&2, Json.Val>) -> List<&2, String>:
90  match xs:
91    case []: []
92    case x <> rest: repository(x) <> repository_items(rest)
93
94def pipeline_items(xs: List<&2, Json.Val>) -> List<&2, String>:
95  match xs:
96    case []: []
97    case x <> rest: pipeline(x) <> pipeline_items(rest)
98
99def connection_items(xs: List<&2, Json.Val>) -> List<&2, String>:
100  match xs:
101    case []: []
102    case x <> rest: connection(x) <> connection_items(rest)
103
104def permission(scope: String, +v: Json.Val) -> String:
105  "A.Permission{" ++ lit(scope) ++ ", " ++ str_list(at(v, "actions")) ++ ", " ++ str_list(at(v, "notActions")) ++ "}"
106
107def permission_items(+scope: String, xs: List<&2, Json.Val>) -> List<&2, String>:
108  match xs:
109    case []: []
110    case x <> rest: permission(scope, x) <> permission_items(scope, rest)
111
112type Rows is Data:
113  Rows{items: List<&2, String>, count: Nat}
114
115def row_items(r: Rows) -> List<&2, String>:
116  match r:
117    case Rows{items, _}: items
118
119def row_count(r: Rows) -> Nat:
120  match r:
121    case Rows{_, count}: count
122
123def add_rows(a: Rows, b: Rows) -> Rows:
124  match a b:
125    case Rows{as, ac} Rows{bs, bc}:
126      Rows{List.append(&2, String, as, bs), Nat.add(ac, bc)}
127
128# Identifiers come from Azure or a project's target fact. Reject shell metacharacters.
129def safe_chars(cs: List<&2, Char>) -> Bool:
130  match cs:
131    case []: True{}
132    case +c <> rest:
133      (Char.is_lower(c) || Char.is_upper(c) || Char.is_digit(c) ||
134       Char.is_eq(c, '-') || Char.is_eq(c, '_') || Char.is_eq(c, '.') ||
135       Char.is_eq(c, ':') || Char.is_eq(c, '/')) && safe_chars(rest)
136
137def safe(+s: String) -> Bool:
138  Bool.not(String.is_empty(s)) && safe_chars(String.to_list(s))
139
140def require_safe_bool(ok: Bool, label: String) -> IO(Unit):
141  match ok:
142    case True{}: IO.pure(Unit, Unit{})
143    case False{}: IO.die(Unit, 1, "snapshot: unsafe or empty " ++ label)
144
145def require_safe(label: String, s: String) -> IO(Unit):
146  require_safe_bool(safe(s), label)
147
148def permissions(+subscription: String, groups: List<&2, Json.Val>) -> IO(Rows):
149  match groups:
150    case []: IO.pure(Rows, Rows{[], 0n})
151    case group <> rest:
152      do IO<Rows>:
153        +name : String = string(at(group, "name"))
154        require_safe("resource group", name)
155        +url : String = "https://management.azure.com/subscriptions/" ++ subscription ++ "/resourceGroups/" ++ name ++ "/providers/Microsoft.Authorization/permissions?api-version=2015-07-01"
156        +data : Json.Val <- read_json("ARM permissions", "az rest --method get --url '" ++ url ++ "' --query 'value[].{actions:actions,notActions:notActions}' -o json")
157        more : Rows <- permissions(subscription, rest)
158        return add_rows(Rows{permission_items(name, array(data)), List.length(&2, Json.Val, array(data))}, more)
159
160def pipeline_details(+org: String, +project_name: String, xs: List<&2, Json.Val>) -> IO(Rows):
161  match xs:
162    case []: IO.pure(Rows, Rows{[], 0n})
163    case x <> rest:
164      do IO<Rows>:
165        +id : String = string(at(x, "id"))
166        require_safe("pipeline id", id)
167        detail : Json.Val <- read_json("DevOps pipeline", "az-devops pipelines show --organization '" ++ org ++ "' --project '" ++ project_name ++ "' --id " ++ id ++ " --query '{name:name,repository:repository.name || configuration.repository.name,yaml:process.yamlFilename || configuration.path}' -o json")
168        more : Rows <- pipeline_details(org, project_name, rest)
169        return add_rows(Rows{[pipeline(detail)], 1n}, more)
170
171def facts_text(+vlib: String, +org: String, +project_name: String, +resources: List<&2, String>, +groups: List<&2, String>, +permissions: List<&2, String>, +projects: List<&2, String>, +repos: List<&2, String>, +pipelines: List<&2, String>, +connections: List<&2, String>, +arm_identity: String, +devops_identity: String) -> String:
172  "import Base\nimport " ++ vlib ++ "/azure.bend as A\n\n# Read-only Azure and Azure DevOps facts. Generated by v system snapshot; do not edit.\n" ++
173  "def organization() -> String:\n  " ++ lit(org) ++ "\n\ndef devops_project() -> String:\n  " ++ lit(project_name) ++ "\n\n" ++
174  "def resources() -> List<&2, A.Resource>:\n  [" ++ String.join(resources, ", ") ++ "]\n\n" ++
175  "def resource_groups() -> List<&2, String>:\n  [" ++ String.join(groups, ", ") ++ "]\n\n" ++
176  "def permissions() -> List<&2, A.Permission>:\n  [" ++ String.join(permissions, ", ") ++ "]\n\n" ++
177  "def projects() -> List<&2, A.Project>:\n  [" ++ String.join(projects, ", ") ++ "]\n\n" ++
178  "def repositories() -> List<&2, A.Repository>:\n  [" ++ String.join(repos, ", ") ++ "]\n\n" ++
179  "def pipelines() -> List<&2, A.Pipeline>:\n  [" ++ String.join(pipelines, ", ") ++ "]\n\n" ++
180  "def service_connections() -> List<&2, A.ServiceConnection>:\n  [" ++ String.join(connections, ", ") ++ "]\n\n" ++
181  "def identities() -> List<&2, A.Identity>:\n  [A.Identity{\"azure\", " ++ lit(arm_identity) ++ "}, A.Identity{\"devops\", " ++ lit(devops_identity) ++ "}]\n"
182
183def snapshot(+f: P.Facts) -> IO(Unit):
184  do IO<Unit>:
185    +org : String = P.azure_organization(f)
186    +project_name : String = P.azure_project(f)
187    require_safe("DevOps organization", org)
188    require_safe("DevOps project", project_name)
189    +account : Json.Val <- read_json("Azure account", "az account show --query '{name:user.name,id:id}' -o json")
190    +subscription : String = string(at(account, "id"))
191    require_safe("subscription", subscription)
192    +resources : Json.Val <- read_json("ARM resources", "az resource list --query '[].{name:name,type:type,resourceGroup:resourceGroup,location:location}' -o json")
193    +groups : Json.Val <- read_json("ARM resource groups", "az group list --query '[].{name:name}' -o json")
194    +grants : Rows <- permissions(subscription, array(groups))
195    +projects : Json.Val <- read_json("DevOps projects", "az-devops devops project list --organization '" ++ org ++ "' --query 'value[].{name:name}' -o json")
196    +repos : Json.Val <- read_json("DevOps repositories", "az-devops repos list --organization '" ++ org ++ "' --project '" ++ project_name ++ "' --query '[].{name:name}' -o json")
197    pipeline_list : Json.Val <- read_json("DevOps pipelines", "az-devops pipelines list --organization '" ++ org ++ "' --project '" ++ project_name ++ "' --query '[].{id:id,name:name}' -o json")
198    +pipeline_rows : Rows <- pipeline_details(org, project_name, array(pipeline_list))
199    +connections : Json.Val <- read_json("DevOps service connections", "az-devops devops service-endpoint list --organization '" ++ org ++ "' --project '" ++ project_name ++ "' --query '[].{name:name,type:type,scheme:authorization.scheme}' -o json")
200    profile : Json.Val <- read_json("DevOps profile", "cat ~/.config/azure-plugin/profiles.json")
201    +devops_identity : String = string(at(at(profile, "devops"), "username"))
202    X.write_file("azure.bend", facts_text(P.vlib_of(f), org, project_name, resource_items(array(resources)), quoted(group_names(array(groups))), row_items(grants), project_items(array(projects)), repository_items(array(repos)), row_items(pipeline_rows), connection_items(array(connections)), string(at(account, "name")), devops_identity))
203    IO.print("snapshot: " ++ Nat.show(List.length(&2, Json.Val, array(resources))) ++ " Azure resources, " ++ Nat.show(row_count(grants)) ++ " permission entries, " ++ Nat.show(List.length(&2, Json.Val, array(groups))) ++ " resource groups; " ++ Nat.show(List.length(&2, Json.Val, array(projects))) ++ " DevOps projects, " ++ Nat.show(List.length(&2, Json.Val, array(repos))) ++ " repos, " ++ Nat.show(row_count(pipeline_rows)) ++ " pipelines, " ++ Nat.show(List.length(&2, Json.Val, array(connections))) ++ " service connections")