package sihl

  1. Overview
  2. Docs

Source file core_configuration.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
let log_src = Logs.Src.create "sihl.core.configuration"

module Logs = (val Logs.src_log log_src : Logs.LOG)

exception Exception of string

type ('ctor, 'ty) schema = (string, 'ctor, 'ty) Conformist.t
type data = (string * string) list

type config =
  { name : string
  ; description : string
  ; type_ : string
  ; default : string option
  }

type t = config list

let make ?schema () =
  match schema with
  | Some schema ->
    Conformist.fold_left
      ~f:(fun res field ->
        let name = Conformist.Field.name field in
        let description =
          Option.value ~default:"-" (Conformist.Field.meta field)
        in
        let type_ = Conformist.Field.type_ field in
        let default = Conformist.Field.encode_default field in
        List.cons
          { name; description; type_; default = CCList.head_opt default }
          res)
      ~init:[]
      schema
  | None -> []
;;

let empty = []

(* We assume a total number of initial configurations of 100 *)
let cache = Hashtbl.create 100

let memoize f arg =
  try Hashtbl.find cache arg with
  | Not_found ->
    let result = f arg in
    (* We don't want to fill up the cache with None *)
    if Option.is_some result then Hashtbl.add cache arg result;
    result
;;

let store data =
  List.iter
    (fun (key, value) ->
      if String.equal "" value
      then ()
      else (
        Hashtbl.replace cache key (Some value);
        Unix.putenv key value))
    data
;;

let envs_to_kv envs =
  envs
  |> List.map (String.split_on_char '=')
  |> List.map (function
       | [] -> "", ""
       | [ key ] -> key, ""
       | [ key; value ] -> key, value
       | key :: values -> key, String.concat "" values)
;;

(* .env file handling *)
let root_path () =
  match Sys.getenv_opt "ROOT_PATH" with
  | None | Some "" ->
    let markers = [ ".git"; ".hg"; ".svn"; ".bzr"; "_darcs" ] in
    let rec find_markers path_els =
      let path = String.concat "/" path_els in
      if List.exists
           (fun marker -> Sys.file_exists (path ^ "/" ^ marker))
           markers
      then (
        (* Path found => Write it into the env var to "memoize" it *)
        Unix.putenv "ROOT_PATH" path;
        Some path)
      else (
        match path_els with
        | [] -> None
        | _ -> find_markers @@ CCList.take (List.length path_els - 1) path_els)
    in
    find_markers @@ String.split_on_char '/' (Unix.getcwd ())
  | Some path -> Some path
;;

let env_files_path () =
  match Sys.getenv_opt "ENV_FILES_PATH" with
  | None | Some "" -> root_path ()
  | Some path -> Some path
;;

let read_env_file () =
  match env_files_path () with
  | Some path ->
    let is_test =
      match Sys.getenv_opt "SIHL_ENV" with
      | Some "test" -> true
      | _ -> false
    in
    let filename = path ^ "/" ^ if is_test then ".env.test" else ".env" in
    let exists = CCIO.File.exists filename in
    if exists
    then (
      Logs.info (fun m -> m "Env file found: %s" filename);
      let envs = CCIO.read_lines_l (open_in filename) in
      Some (envs_to_kv envs))
    else (
      Logs.info (fun m ->
        m "Env file not found: %s. Continuing without it." filename);
      None)
  | None ->
    Logs.debug (fun m ->
      m
        "No env files directory found, please provide your own directory path \
         with environment variable ENV_FILES_PATH if you would like to use env \
         files");
    None
;;

let file_was_read = ref false

let load_env_file () =
  if !file_was_read
  then (* Nothing to do, the file was read already *) ()
  else (
    let file_configuration = read_env_file () in
    file_was_read := true;
    store (Option.value file_configuration ~default:[]))
;;

let environment_variables () =
  load_env_file ();
  Unix.environment () |> Array.to_list |> envs_to_kv
;;

let read schema =
  let data = environment_variables () in
  let data = List.map (fun (k, v) -> k, [ v ]) data in
  match Conformist.decode_and_validate schema data with
  | Ok value -> value
  | Error errors ->
    let errors =
      List.map
        (fun (field, input, msg) ->
          match CCList.head_opt input with
          | None ->
            Format.sprintf "Failed to read configuration '%s': %s" field msg
          | Some input ->
            Format.sprintf
              "Failed to read configuration '%s' for '%s': %s"
              input
              field
              msg)
        errors
    in
    List.iter (fun error -> Logs.err (fun m -> m "%s" error)) errors;
    raise (Exception "Invalid configuration provided")
;;

let read_string' key =
  load_env_file ();
  Sys.getenv_opt key
;;

let read_string = memoize read_string'

let load () =
  load_env_file ();
  match read_string "SIHL_ENV" with
  | None ->
    Logs.info (fun m ->
      m
        "SIHL_ENV not found. Set it to one of the following values: \
         development, production, test");
    failwith "SIHL_ENV not found"
  | Some env -> Logs.info (fun m -> m "SIHL_ENV: %s" env)
;;

let is_test () =
  match read_string "SIHL_ENV" with
  | Some "test" -> true
  | _ -> false
;;

let is_development () =
  match read_string "SIHL_ENV" with
  | Some "development" -> true
  | _ -> false
;;

let is_production () =
  match read_string "SIHL_ENV" with
  | Some "production" -> true
  | _ -> false
;;

let read_secret () =
  match is_production (), read_string "SIHL_SECRET" with
  | true, Some secret ->
    (* TODO [jerben] provide proper security policy (entropy or smth) *)
    if String.length secret > 10
    then secret
    else (
      Logs.err (fun m -> m "SIHL_SECRET has to be longer than 10");
      failwith "Insecure secret provided")
  | false, Some secret -> secret
  | true, None ->
    Logs.err (fun m -> m "Set SIHL_SECRET before deploying Sihl to production");
    failwith "No secret provided"
  (* In testing and local dev we don't have to use real security *)
  | false, None -> "secret"
;;

let read_int key =
  match read_string key with
  | Some value -> int_of_string_opt value
  | None -> None
;;

let read_bool key =
  match read_string key with
  | Some "1" | Some "true" | Some "True" -> Some true
  | Some "0" | Some "false" | Some "False" -> Some false
  | _ -> None
;;

let require schema = read schema |> ignore

(* Displaying configurations *)

let configuration_to_string (configurations : t) : string =
  configurations
  |> List.map (fun { name; description; type_; default } ->
       match default with
       | Some default ->
         Format.sprintf
           {|
%s
%s
Type: %s
Default: %s
|}
           name
           description
           type_
           default
       | None ->
         Format.sprintf {|
%s
%s
Type: %s
Required
|} name description type_)
  |> String.concat ""
;;

let print_cmd (configurations : t list) : Core_command.t =
  Core_command.make
    ~name:"config"
    ~description:"Prints a list of configurations that are known to Sihl."
    (fun _ ->
    configurations
    |> List.filter (fun configuration -> List.length configuration > 0)
    |> List.concat
    |> List.sort (fun c1 c2 ->
         (* We want to show required configurations first. *)
         match c1.default, c2.default with
         | Some _, Some _ -> 0
         | Some _, None -> 1
         | None, Some _ -> -1
         | None, None -> 0)
    |> configuration_to_string
    |> print_endline
    |> Option.some
    |> Lwt.return)
;;

let commands configurations = [ print_cmd configurations ]
OCaml

Innovation. Community. Security.