diff --git a/all-apps/authelia/4/Caddyfile b/all-apps/authelia/4/Caddyfile
index 7965d23..0e1d5a7 100644
--- a/all-apps/authelia/4/Caddyfile
+++ b/all-apps/authelia/4/Caddyfile
@@ -14,8 +14,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with Nassella. If not, see .
-{$authelia_SUBDOMAIN}.{$ROOT_DOMAIN} {
- import maintenance_intercept
+#{$authelia_SUBDOMAIN}.{$ROOT_DOMAIN} {
+# route {
+# @authelia path /authelia /authelia/*
+# handle @authelia {
+# reverse_proxy authelia:9091
+# }
+# forward_auth authelia:9091 {
+# uri /api/authz/forward-auth
+# copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
+# }
+# reverse_proxy authelia:9091
+# }
+#}
- reverse_proxy authelia:9091
-}
diff --git a/all-apps/authelia/4/Makefile b/all-apps/authelia/4/Makefile
index 6faac37..effe7fa 100644
--- a/all-apps/authelia/4/Makefile
+++ b/all-apps/authelia/4/Makefile
@@ -18,7 +18,8 @@ IMAGE_DEPS := $(IMAGE_DEPS) \
app/authelia/postgres_db \
app/authelia/postgres_user \
app/authelia/postgres_password \
-app/authelia/authelia-config/configuration.yml
+app/authelia/authelia-config/configuration.yml \
+app/authelia/authelia-config/users.yml
app/authelia/postgres_db: $(apps_config)
bash -c 'source ./$(apps_config); printf "%s\n" "$$AUTHELIA_POSTGRES_DB" > $@'
@@ -29,3 +30,6 @@ app/authelia/postgres_password: $(apps_config)
app/authelia/authelia-config/configuration.yml: $(apps_config) app/authelia/authelia-config/configuration.yml.tmpl \
app/authelia/make-authelia-config.sh
./app/authelia/make-authelia-config.sh $(apps_config)
+app/authelia/authelia-config/users.yml: $(apps_config) app/authelia/authelia-config/users.yml.tmpl \
+app/authelia/make-authelia-users-config.sh
+ ./app/authelia/make-authelia-users-config.sh $(apps_config)
diff --git a/all-apps/authelia/4/authelia-config/configuration.yml.tmpl b/all-apps/authelia/4/authelia-config/configuration.yml.tmpl
index 80cb9fe..e73ceeb 100644
--- a/all-apps/authelia/4/authelia-config/configuration.yml.tmpl
+++ b/all-apps/authelia/4/authelia-config/configuration.yml.tmpl
@@ -35,58 +35,45 @@ identity_validation:
reset_password:
jwt_secret: '$AUTHELIA_JWT_SECRET'
-# lldap service account user should instead
-# use an account with lldap_password_manager group
-# since that can't be used to change an admin password
authentication_backend:
- ldap:
- address: 'ldap://lldap:3890'
- implementation: 'lldap'
- timeout: '5s'
- pooling:
- enable: false
- count: 5
- retries: 2
- timeout: '10 seconds'
- base_dn: 'DC=nassella,DC=org'
-# additional_users_dn: 'OU=users'
-# additional_groups_dn: 'OU=groups'
-# group_search_mode: 'filter'
-# permit_referrals: false
- permit_unauthenticated_bind: false
- permit_feature_detection_failure: false
- user: 'uid=admin,ou=people,dc=nassella,dc=org'
- password: '$LLDAP_ADMIN_PASSWORD'
- # attributes:
- # distinguished_name: 'distinguishedName'
- # username: 'uid'
- # display_name: 'displayName'
- # family_name: 'sn'
- # given_name: 'givenName'
- # middle_name: 'middleName'
- # nickname: ''
- # gender: ''
- # birthdate: ''
- # website: 'wWWHomePage'
- # profile: ''
- # picture: ''
- # zoneinfo: ''
- # locale: ''
- # phone_number: 'telephoneNumber'
- # phone_extension: ''
- # street_address: 'streetAddress'
- # locality: 'l'
- # region: 'st'
- # postal_code: 'postalCode'
- # country: 'c'
- # mail: 'mail'
- # member_of: 'memberOf'
- # group_name: 'cn'
- # extra:
- # extra_example:
- # name: ''
- # multi_valued: false
- # value_type: 'string'
+ disable_reset_password: true
+ file:
+ path: '/config/users.yml'
+ watch: false
+ search:
+ email: false
+ case_insensitive: false
+ extra_attributes:
+ extra_example:
+ multi_valued: false
+ value_type: 'string'
+ password:
+ algorithm: 'argon2'
+ argon2:
+ variant: 'argon2id'
+ iterations: 3
+ memory: 65536
+ parallelism: 4
+ key_length: 32
+ salt_length: 16
+ scrypt:
+ variant: 'scrypt'
+ iterations: 16
+ block_size: 8
+ parallelism: 1
+ key_length: 32
+ salt_length: 16
+ pbkdf2:
+ variant: 'sha512'
+ iterations: 310000
+ salt_length: 16
+ sha2crypt:
+ variant: 'sha512'
+ iterations: 50000
+ salt_length: 16
+ bcrypt:
+ variant: 'standard'
+ cost: 12
access_control:
default_policy: 'deny'
@@ -102,11 +89,12 @@ session:
secret: '$AUTHELIA_SESSION_SECRET'
cookies:
- - name: 'authelia_session'
- domain: '$ROOT_DOMAIN' # Should match whatever your root protected domain is
- authelia_url: 'https://$AUTHELIA_FULL_DOMAIN'
+ - name: 'dozzle_session'
+ domain: '$DOZZLE_FULL_DOMAIN' # Should match whatever your root protected domain is
+ authelia_url: 'https://$DOZZLE_FULL_DOMAIN/authelia'
expiration: '1 hour' # 1 hour
inactivity: '5 minutes' # 5 minutes
+ default_redirection_url: 'https://$DOZZLE_FULL_DOMAIN/'
regulation:
max_retries: 3
diff --git a/all-apps/authelia/4/docker-compose.yaml b/all-apps/authelia/4/docker-compose.yaml
index e3b93fd..819a766 100644
--- a/all-apps/authelia/4/docker-compose.yaml
+++ b/all-apps/authelia/4/docker-compose.yaml
@@ -50,13 +50,11 @@ services:
image: 'authelia/authelia'
volumes:
- ./authelia/authelia-config/configuration.yml:/config/configuration.yml:ro
+ - ./authelia/authelia-config/users.yml:/config/users.yml:ro
networks:
- lb
- authelia_internal_db
- - lldap_internal
depends_on:
- lldap:
- condition: service_healthy
authelia_db:
condition: service_healthy
restart: 'unless-stopped'
@@ -66,9 +64,6 @@ services:
networks:
lb:
- lldap_internal:
- driver: bridge
- internal: true
authelia_internal_db:
driver: bridge
internal: true
diff --git a/all-apps/dozzle/20/Caddyfile b/all-apps/dozzle/20/Caddyfile
index a14c0db..3080b49 100644
--- a/all-apps/dozzle/20/Caddyfile
+++ b/all-apps/dozzle/20/Caddyfile
@@ -15,9 +15,15 @@
# along with Nassella. If not, see .
{$dozzle_SUBDOMAIN}.{$ROOT_DOMAIN} {
- forward_auth authelia:9091 {
- uri /api/authz/forward-auth
- copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
+ route {
+ @authelia path /authelia /authelia/*
+ handle @authelia {
+ reverse_proxy authelia:9091
+ }
+ forward_auth authelia:9091 {
+ uri /api/authz/forward-auth
+ copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
+ }
+ reverse_proxy http://dozzle:8080
}
- reverse_proxy http://dozzle:8080
}
diff --git a/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl b/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl
index bc3a7f9..ab35ebc 100644
--- a/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl
+++ b/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl
@@ -84,7 +84,7 @@ access_control:
policy: 'two_factor'
session:
- secret: 'insecure_session_secret'
+ secret: 'insecure_session_secret' # TODO fix
cookies:
- name: 'authelia_session'
diff --git a/src/Dockerfile b/src/Dockerfile
index 9826dfe..cb55f43 100644
--- a/src/Dockerfile
+++ b/src/Dockerfile
@@ -45,6 +45,7 @@ RUN chmod +x nassella-run
FROM quay.io/coreos/butane:release AS butane
FROM hashicorp/terraform:1.15 AS terraform
+FROM authelia/authelia:latest AS authelia
FROM debian:trixie-slim
RUN apt-get update && apt-get -y --no-install-recommends install \
@@ -53,6 +54,7 @@ RUN apt-get update && apt-get -y --no-install-recommends install \
COPY --from=butane /usr/local/bin/butane /usr/local/bin/butane
COPY --from=terraform /bin/terraform /usr/local/bin/terraform
+COPY --from=authelia /app/authelia /usr/local/bin/authelia
COPY --from=buildeggs /usr/local/ /usr/local/
diff --git a/src/nassella.scm b/src/nassella.scm
index 1f7d7e5..7037278 100644
--- a/src/nassella.scm
+++ b/src/nassella.scm
@@ -869,67 +869,37 @@ h1, h2, h3, h4, h5, h6 {
`((success . #f)
(result ,data))))))
-;; (define (test-backblaze-connection user-id instance-id key-id application-key bucket-url)
-;; (let* ((password-path (conc "restic-password-" user-id "-" instance-id))
-;; (restic-password
-;; (with-db/transaction
-;; (lambda (db)
-;; (get-instance-restic-password db user-id instance-id)))))
-;; (dynamic-wind
-;; (lambda ()
-;; (with-output-to-file password-path (lambda () (display restic-password))))
-;; (lambda ()
-;; ;; restic retries (indefinitely?) if the connection can't be made so
-;; ;; we pass it through timeout to ensure it does not hang
-;; (receive (in-port out-port pid err-port)
-;; (cond-expand
-;; (dev
-;; (process* "timeout" `("--preserve-status" "8s" "docker" "run" "--rm" "--volume"
-;; ,(conc (current-directory) "/" password-path ":/restic-password")
-;; "-e" ,(conc "AWS_ACCESS_KEY_ID=" key-id)
-;; "-e" ,(conc "AWS_SECRET_ACCESS_KEY=" application-key)
-;; "-i" "restic/restic:0.18.0" "cat" "config"
-;; "--repo" ,(conc "s3:" bucket-url)
-;; "--password-file" "/restic-password"
-;; "--json")))
-;; (else
-;; (process* "/bin/timeout"
-;; `("--preserve-status" "8s" "/bin/restic" "cat" "config"
-;; "--repo" ,(conc "s3:" bucket-url)
-;; "--password-file" ,(conc (current-directory) "/" password-path)
-;; "--json")
-;; `(("AWS_ACCESS_KEY_ID" . ,key-id)
-;; ("AWS_SECRET_ACCESS_KEY" . ,application-key)))))
-;; (let ((thread
-;; (thread-start!
-;; (lambda ()
-;; (let loop ((i 0))
-;; (thread-sleep! 1)
-;; ;; We do a non-blocking wait here so that we don't
-;; ;; block the entire web process.
-;; (receive (wait-pid exit-normal status) (process-wait pid #t)
-;; (if (= wait-pid 0) ;; wait-pid is 0 until the process has finished
-;; (if (< i 12) ;; 12s timeout
-;; (loop (+ i 1))
-;; `((success . #f)
-;; (errors ((message . "timeout trying to connect to backblaze")))))
-;; (if exit-normal
-;; (let ((res (with-input-from-port in-port read-json))
-;; (err (with-input-from-port err-port read-json)))
-;; ;; status 10 is what is returned if we can connect but the repo
-;; ;; is not initialized yet
-;; (if (or res (and err (= status 10)))
-;; `((success . #t)
-;; (result . ,res))
-;; `((success . #f)
-;; (errors ((message . "abnormal exit - check Key ID, Application ID, and Bucket URL"))))))
-;; `((success . #f)
-;; (errors ((message . "abnormal exit - check Key ID, Application ID, and Bucket URL"))))
-;; ))))))))
-;; (thread-join! thread))))
-;; (lambda ()
-;; (handle-exceptions exn 'ignore
-;; (delete-file password-path))))))
+(define *instance-up-timeout* 1) ;; seconds
+(define *instance-up-retry-time* 120) ;; 120s, 2 minutes
+(define *instance-up-retries* (/ *instance-up-retry-time* *instance-up-timeout*))
+(define (instance-up? path)
+ (let loop ((i 0))
+ (let ((thread
+ (thread-start!
+ (lambda ()
+ (condition-case
+ (receive (data uri response) (with-input-from-request path #f read-string)
+ (if (eq? (response-code response) 401)
+ #t
+ (begin (thread-sleep! *instance-up-timeout*) ;; we depend on this taking at least as long as the timeout if the instance is still booting
+ #f)))
+ (e (exn client-error)
+ (if (eq? (response-code (get-condition-property e 'client-error 'response)) 401)
+ #t
+ (begin (thread-sleep! *instance-up-timeout*) ;; we depend on this taking at least as long as the timeout if the instance is still booting
+ #f)))
+ (e (exn net)
+ (thread-sleep! *instance-up-timeout*)
+ #f)
+ (e ()
+ (thread-sleep! *instance-up-timeout*)
+ #f))))))
+ (if (thread-join! thread *instance-up-timeout* #f)
+ #t
+ (if (< i *instance-up-retries*)
+ (loop (+ i 1))
+ #f)))))
+
(define (deployment-directory user-id instance-id)
(string-append "deploy-" (number->string user-id) "-" (->string instance-id)))
@@ -1077,6 +1047,34 @@ chmod -R 777 /opt/keys")))
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
64)))
+(define (create-authelia-password-hash password)
+ ;; docker run --rm -it authelia/authelia:latest authelia crypto hash generate argon2
+ (receive (in-port out-port pid err-port)
+ (cond-expand
+ (dev
+ (process* "docker" `("run" "--rm" "authelia/authelia:latest" "authelia" "crypto" "hash" "generate" "argon2" "--password" ,password)))
+ (else
+ (process* "/usr/local/bin/authelia" `("crypto" "hash" "generate" "argon2" "--password" ,password))))
+ (let ((thread
+ (thread-start!
+ (lambda ()
+ (let loop ()
+ (thread-sleep! 1)
+ ;; We do a non-blocking wait here so that we don't
+ ;; block the entire web process.
+ (receive (wait-pid exit-normal status) (process-wait pid #t)
+ (if (= wait-pid 0) ;; wait-pid is 0 until the process has finished
+ (loop)
+ (if exit-normal
+ (begin
+ (with-input-from-port err-port read-string) ;; left here for debugging and to clear ports
+ ;; the output is prefixed with "Digest: " and suffixed with a newline
+ ;; so we clean it up
+ (string-drop (string-trim-right (with-input-from-port in-port read-string)) 8))
+ (begin (log-to (debug-log) "authelia: docker command error")
+ (error "Authelia for generating password hash had abnormal exit"))))))))))
+ (thread-join! thread))))
+
(define (generate-postgres-password)
(generator->string (gtake (make-random-char-generator
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
@@ -1401,8 +1399,9 @@ chmod -R 777 /opt/keys")))
'()))
(Field (@ (name "wordpress") (type "checkbox") (label ("Wordpress")) (checked ,(member 'wordpress (alist-ref 'selected-apps results)))))
(Field (@ (name "log-viewer") (type "checkbox") (label ("Log Viewer")) (checked #t) (disabled "disabled")))
- (Field (@ (name "lldap") (type "checkbox") (label ("Admin LLDAP")) (checked #t) (disabled "disabled")))
- (Field (@ (name "authelia") (type "checkbox") (label ("Admin Authelia")) (checked #t) (disabled "disabled"))))
+ ;; (Field (@ (name "lldap") (type "checkbox") (label ("Admin LLDAP")) (checked #t) (disabled "disabled")))
+ ;; (Field (@ (name "authelia") (type "checkbox") (label ("Admin Authelia")) (checked #t) (disabled "disabled")))
+ )
;; TODO add config for when automatic upgrades are scheduled for?
;; TODO add config for server timezone?
(Form-Nav (@ (back-to ,(conc "/config/wizard/services-success/" instance-id))))))))))
@@ -1424,7 +1423,7 @@ chmod -R 777 /opt/keys")))
(ghost . ,(or (and (alist-ref 'ghost (current-params)) "6") #f))
(nassella . ,(or (and (alist-ref 'nassella (current-params)) "b0.0.1") #f))
(wordpress . ,(or (and (alist-ref 'wordpress (current-params)) "8.4") #f))
- (lldap . "0.6.3")
+ (lldap . #f) ;; "0.6.3"
(authelia . "4")
(instance-control . "b0.0.1")
(log-viewer . "20"))))
@@ -1500,31 +1499,30 @@ chmod -R 777 /opt/keys")))
(Field (@ (name "wordpress-subdomain") (label ("Subdomain")) (required #t)
(value ,(alist-ref 'subdomain (alist-ref 'wordpress app-config eq? '()) eq? "wordpress"))))))
'())
- ,@(if (member 'lldap selected-apps)
- `((Fieldset
- (@ (title "Admin LLDAP"))
- (Field (@ (name "lldap-subdomain") (label ("Subdomain")) (required #t)
- (value ,(alist-ref 'subdomain (alist-ref 'lldap app-config eq? '()) eq? "lldap"))))
- (Field (@ (name "lldap-user-email") (label ("Admin Email Address")) (type "email") (required #t)
- (value ,(alist-ref 'user-email (alist-ref 'lldap app-config eq? '()) eq? ""))))
- (Field (@ (name "lldap-admin-password") (label ("Admin Password")) (type "password") (required #t)
- (value ,(alist-ref 'admin-password (alist-ref 'lldap app-config eq? '()) eq? ""))))))
- '())
- ,@(if (member 'authelia selected-apps)
- `((Fieldset
- (@ (title "Admin Authelia"))
- (Field (@ (name "authelia-subdomain") (label ("Subdomain")) (required #t)
- (value ,(alist-ref 'subdomain (alist-ref 'authelia app-config eq? '()) eq? "authelia"))))))
- '())
+ ;; ,@(if (member 'lldap selected-apps)
+ ;; `((Fieldset
+ ;; (@ (title "Admin LLDAP"))
+ ;; (Field (@ (name "lldap-subdomain") (label ("Subdomain")) (required #t)
+ ;; (value ,(alist-ref 'subdomain (alist-ref 'lldap app-config eq? '()) eq? "lldap"))))
+ ;; (Field (@ (name "lldap-user-email") (label ("Admin Email Address")) (type "email") (required #t)
+ ;; (value ,(alist-ref 'user-email (alist-ref 'lldap app-config eq? '()) eq? ""))))
+ ;; (Field (@ (name "lldap-admin-password") (label ("Admin Password")) (type "password") (required #t)
+ ;; (value ,(alist-ref 'admin-password (alist-ref 'lldap app-config eq? '()) eq? ""))))))
+ ;; '())
+ ;; ,@(if (member 'authelia selected-apps)
+ ;; `((Fieldset
+ ;; (@ (title "Admin Authelia"))
+ ;; (Field (@ (name "authelia-subdomain") (label ("Subdomain")) (required #t)
+ ;; (value ,(alist-ref 'subdomain (alist-ref 'authelia app-config eq? '()) eq? "authelia"))))))
+ ;; '())
(Fieldset
(@ (title "Log Viewer"))
(Field (@ (name "log-viewer-subdomain") (label ("Subdomain")) (required #t)
(value ,(alist-ref 'subdomain (alist-ref 'log-viewer app-config eq? '()) eq? "logs"))))
- ;; (Field (@ (name "log-viewer-user") (label ("Username")) (required #t)
- ;; (value ,(alist-ref 'user (alist-ref 'log-viewer app-config eq? '()) eq? ""))))
- ;; (Field (@ (name "log-viewer-password") (label ("Password")) (type "password") (required #t)
- ;; (value ,(alist-ref 'password (alist-ref 'log-viewer app-config eq? '()) eq? ""))))
- )
+ (Field (@ (name "log-viewer-user") (label ("Admin Email")) (required #t)
+ (value ,(alist-ref 'user (alist-ref 'log-viewer app-config eq? '()) eq? ""))))
+ (Field (@ (name "log-viewer-password") (label ("Admin Password")) (type "password") (required #t)
+ (value ,(alist-ref 'password (alist-ref 'log-viewer app-config eq? '()) eq? "")))))
,@(if (or (member 'nextcloud selected-apps) (member 'ghost selected-apps) (member 'nassella selected-apps) (member 'authelia selected-apps))
`((Fieldset
(@ (title "All Apps - Email - SMTP"))
@@ -1662,8 +1660,8 @@ chmod -R 777 /opt/keys")))
(alist-ref 'authelia config eq? '()))
(generate-authelia-key-seed)))))
(log-viewer . ((subdomain . ,(alist-ref 'log-viewer-subdomain (current-params)))
- (user . "unused") ;; TODO can we remove?
- (password . "unused"))) ;; TODO can we remove?
+ (user . ,(alist-ref 'log-viewer-user (current-params)))
+ (password . ,(alist-ref 'log-viewer-password (current-params)))))
(all-apps . ((smtp-host . ,(alist-ref 'smtp-host (current-params)))
(smtp-port . ,(alist-ref 'smtp-port (current-params)))
(smtp-auth-user . ,(alist-ref 'smtp-auth-user (current-params)))
@@ -1836,13 +1834,14 @@ chmod -R 777 /opt/keys")))
(map (lambda (app-version)
(conc (if (eq? (car app-version) 'log-viewer) 'dozzle (car app-version))
","
- (alist-ref 'subdomain (alist-ref (car app-version) config))
+ (or (alist-ref 'subdomain (alist-ref (car app-version) config))
+ "nassella-ignore") ;; a hack to get authelia to work for now
","
(cdr app-version)))
selected-apps)
" "))
("HOST_ADMIN_USER" . ,(alist-ref 'user (alist-ref 'log-viewer config)))
- ("HOST_ADMIN_PASSWORD" . ,(alist-ref 'password (alist-ref 'log-viewer config)))
+ ("HOST_ADMIN_PASSWORD" . ,(create-authelia-password-hash (alist-ref 'password (alist-ref 'log-viewer config))))
("NEXTCLOUD_ADMIN_USER" . ,(alist-ref 'admin-user (alist-ref 'nextcloud config)))
("NEXTCLOUD_ADMIN_PASSWORD" . ,(alist-ref 'admin-password (alist-ref 'nextcloud config)))
("NEXTCLOUD_POSTGRES_DB" . "nextcloud")
@@ -1999,13 +1998,29 @@ chmod -R 777 /opt/keys")))
(with-db/transaction
(lambda (db)
(update-deployment-progress db deployment-id progress)
- (update-deployment-status
- db user-id deployment-id
- (if (= status 0) 'complete 'failed)
- (with-input-from-file (string-append dir "/make-out") read-string))
(update-user-terraform-state db user-id instance-id
(if (eof-object? tf-state) "" tf-state)
- (if (eof-object? tf-state-backup) "" tf-state-backup))))))))))))))
+ (if (eof-object? tf-state-backup) "" tf-state-backup))
+ (if (= status 0)
+ (update-deployment-progress db deployment-id `((instance-up . in-progress)))
+ (update-deployment-status
+ db user-id deployment-id
+ 'failed
+ (with-input-from-file (string-append dir "/make-out") read-string)))))
+ (when (= status 0)
+ (let ((instance-up (instance-up? (string-append "https://"
+ (alist-ref 'subdomain (alist-ref 'log-viewer (alist-ref 'config app-config)))
+ "."
+ (alist-ref 'root-domain app-config)))))
+ (with-db/transaction
+ (lambda (db)
+ (update-deployment-progress db deployment-id `((instance-up . ,(if instance-up 'complete 'failed))))
+ (update-deployment-status
+ db user-id deployment-id
+ (if instance-up
+ 'complete
+ 'failed)
+ (with-input-from-file (string-append dir "/make-out") read-string))))))))))))))))
(redirect (conc "/config/wizard/success/" (alist-ref "id" (current-params) equal?))))
(get/widgets
@@ -2057,7 +2072,8 @@ chmod -R 777 /opt/keys")))
(ip-destroy . "Instance Mapped IP Disconnect")
(ip-create . "Instance Mapped IP Connect")
(volume-create . "App Volume Connect")
- (machine-destroy . "Cleanup Previous Instance"))))
+ (machine-destroy . "Cleanup Previous Instance")
+ (instance-up . "Instance Starting Up"))))
(form
(@ (action "/dashboard") (method GET))
,@(if (or (eq? (string->symbol status) 'complete) (eq? (string->symbol status) 'failed))