Compare commits

...

28 Commits

Author SHA1 Message Date
Lucy C
11b007a31d Fix/integration/0.2.11 (#265)
* backports tor security fix to 0.2.10, adds functionality to allow for ssh key management during an update (#263)

* actually upgrade to 0.3.5.14-1

* update lan services on backup restore

* reload nginx, update welcome message, move reset lan to handler

* moves lan refresh after backup restore to asynchronous part of restore

* fix certificate generation

* match guards

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-19 16:50:52 -06:00
Lucy C
5b8f27e53e Appmgr/fix/restart dep on install (#262)
* appmgr: fix: restart dep on install

* appmgr: fix: bind before restart

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-18 00:46:29 -06:00
Aiden McClelland
9f4523676f appmgr: fix: restart dep on install (#261) 2021-03-17 22:08:12 -06:00
Lucy C
bc5163d800 Appmgr/debug/uninstall (#260)
* with context debuggging

* appmgr: fix errors

* appmgr: add more logging

* appmgr: more logs

* appmgr: make unbind more robust

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-17 18:55:51 -06:00
Lucy C
9b7fe03c19 copy updates for 0.2.11 (#248)
* copy updates for 0.2.11

* fix moderate npm vulns

* fix mocks and welcome page layout
2021-03-17 12:30:41 -06:00
Lucy Cifferello
9a2aaa08b8 ignore ds_store 2021-03-17 12:30:41 -06:00
Lucy Cifferello
8c87e6653c fix import 2021-03-17 12:30:41 -06:00
Chris Guida
1c3b16e870 Appmgr/bugfix/nginx file too large migrations (#258)
* Nginx: allow infinite body sizes

* add nginx refresh to migration script
2021-03-17 12:30:41 -06:00
Lucy Cifferello
276085f084 fix mocks file 2021-03-17 12:30:41 -06:00
Lucy Cifferello
52fc992090 update welcome page release notes 2021-03-17 12:30:41 -06:00
Chris Guida
af46a375a9 Nginx: allow infinite body sizes (#256) 2021-03-17 12:30:41 -06:00
Aiden McClelland
74a559eade ui: add elements to union page 2021-03-17 12:30:41 -06:00
Aiden McClelland
f12d97122a ui: edited dot on union enum 2021-03-17 12:30:41 -06:00
Aiden McClelland
ba9b3519de ui: fix isEdited for unions 2021-03-17 12:30:41 -06:00
Aiden McClelland
43e89df652 appmgr: handle optional dep unmounts on uninstall 2021-03-17 12:30:41 -06:00
Keagan McClelland
7bdc109bd4 retry once on exit 6 for list 2021-03-16 11:20:33 -06:00
Lucy C
ac5dec476d copy updates for 0.2.11 (#248)
* copy updates for 0.2.11

* fix moderate npm vulns

* fix mocks and welcome page layout
2021-03-12 15:42:18 -07:00
Keagan McClelland
1f56be3cbf 0.2.11 bump 2021-03-12 15:42:18 -07:00
Keagan McClelland
ed46ddbf44 removes default welcome to nginx page (#247) 2021-03-12 15:42:18 -07:00
Keagan McClelland
2973c316a8 catches if either file doesn't exist and runs the sync if so (#245) 2021-03-12 15:42:18 -07:00
Aiden McClelland
7e7a9dc140 appmgr: version bump 2021-03-12 15:42:18 -07:00
Aiden McClelland
b95686282d ui: fix union error message 2021-03-12 15:42:18 -07:00
Aiden McClelland
09f858d28d appmgr: only mount binds if installed
also improve build scripts
2021-03-12 15:42:18 -07:00
Keagan McClelland
424afb3d1c adds actions copy (#240)
* adds actions copy

* punctuation

* punctuation

* removes superfluous language
2021-03-08 17:51:27 -07:00
Matt Hill
a056f6d318 fix instructions link (#239) 2021-03-08 17:25:24 -07:00
Keagan McClelland
5339b23ea6 0.2.10 version bump stuff (#238)
* agent -> 0.2.10

* appmgr -> 0.2.10

* updates to 0.2.10 for the UI

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-08 16:38:24 -07:00
Aiden McClelland
bd61510c24 Hotfix/rewrite certs (#237)
* appmgr: rewrite full cert if missing

* rewrite if key missing
2021-03-08 16:25:05 -07:00
Keagan McClelland
91557c39e5 drops uniqueness requirement (#236) 2021-03-08 16:21:21 -07:00
41 changed files with 17154 additions and 1119 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store
/*.img
/buster.zip
/product_key

View File

@@ -1,9 +1,9 @@
# Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable.
# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables
static-dir: "_env:YESOD_STATIC_DIR:static"
host: "_env:YESOD_HOST:*4" # any IPv4 host
port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
static-dir: "_env:YESOD_STATIC_DIR:static"
host: "_env:YESOD_HOST:*4" # any IPv4 host
port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
ip-from-header: "_env:YESOD_IP_FROM_HEADER:false"
detailed-logging: "_env:DETAILED_LOGGING:false"
@@ -33,6 +33,5 @@ database:
database: "start9_agent.sqlite3"
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
app-mgr-version-spec: "=0.2.9"
app-mgr-version-spec: "=0.2.11"
#analytics: UA-YOURCODE

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -1,117 +1,117 @@
name: ambassador-agent
version: 0.2.9
version: 0.2.11
default-extensions:
- NoImplicitPrelude
- BlockArguments
- ConstraintKinds
- DataKinds
- DeriveAnyClass
- DeriveFunctor
- DeriveGeneric
- DerivingStrategies
- EmptyCase
- FlexibleContexts
- FlexibleInstances
- GADTs
- GeneralizedNewtypeDeriving
- InstanceSigs
- KindSignatures
- LambdaCase
- MultiParamTypeClasses
- MultiWayIf
- NamedFieldPuns
- NumericUnderscores
- OverloadedStrings
- PolyKinds
- RankNTypes
- StandaloneDeriving
- StandaloneKindSignatures
- TupleSections
- TypeApplications
- TypeFamilies
- TypeOperators
- NoImplicitPrelude
- BlockArguments
- ConstraintKinds
- DataKinds
- DeriveAnyClass
- DeriveFunctor
- DeriveGeneric
- DerivingStrategies
- EmptyCase
- FlexibleContexts
- FlexibleInstances
- GADTs
- GeneralizedNewtypeDeriving
- InstanceSigs
- KindSignatures
- LambdaCase
- MultiParamTypeClasses
- MultiWayIf
- NamedFieldPuns
- NumericUnderscores
- OverloadedStrings
- PolyKinds
- RankNTypes
- StandaloneDeriving
- StandaloneKindSignatures
- TupleSections
- TypeApplications
- TypeFamilies
- TypeOperators
dependencies:
- base >=4.9.1.0 && <5
- aeson
- aeson-flatten
- attoparsec
- bytestring
- casing
- comonad
- conduit
- conduit-extra
- connection
- containers
- cryptonite
- cryptonite-conduit
- data-default
- directory
- errors
- exceptions
- exinst
- fast-logger
- file-embed
- filelock
- filepath
- fused-effects
- fused-effects-th
- git-embed
- http-api-data
- http-client
- http-client-tls
- http-conduit
- http-types
- interpolate
- iso8601-time
- json-rpc
- lens
- lens-aeson
- lifted-async
- lifted-base
- memory
- mime-types
- monad-control
- monad-logger
- network
- persistent
- persistent-sqlite
- persistent-template
- process
- process-extras
- protolude
- resourcet
- regex-compat # TODO: trim this dep
- shell-conduit
- singletons
- stm
- streaming
- streaming-bytestring
- streaming-conduit
- streaming-utils
- tar-conduit
- template-haskell
- text >=0.11 && <2.0
- time
- transformers
- transformers-base
- typed-process
- unix
- unliftio # TODO: trim this dep
- unliftio-core # TODO: trim this dep
- unordered-containers
- uuid
- wai
- wai-cors
- wai-extra
- warp
- yaml
- yesod
- yesod-auth
- yesod-core
- yesod-form
- yesod-persistent
- base >=4.9.1.0 && <5
- aeson
- aeson-flatten
- attoparsec
- bytestring
- casing
- comonad
- conduit
- conduit-extra
- connection
- containers
- cryptonite
- cryptonite-conduit
- data-default
- directory
- errors
- exceptions
- exinst
- fast-logger
- file-embed
- filelock
- filepath
- fused-effects
- fused-effects-th
- git-embed
- http-api-data
- http-client
- http-client-tls
- http-conduit
- http-types
- interpolate
- iso8601-time
- json-rpc
- lens
- lens-aeson
- lifted-async
- lifted-base
- memory
- mime-types
- monad-control
- monad-logger
- network
- persistent
- persistent-sqlite
- persistent-template
- process
- process-extras
- protolude
- resourcet
- regex-compat # TODO: trim this dep
- shell-conduit
- singletons
- stm
- streaming
- streaming-bytestring
- streaming-conduit
- streaming-utils
- tar-conduit
- template-haskell
- text >=0.11 && <2.0
- time
- transformers
- transformers-base
- typed-process
- unix
- unliftio # TODO: trim this dep
- unliftio-core # TODO: trim this dep
- unordered-containers
- uuid
- wai
- wai-cors
- wai-extra
- warp
- yaml
- yesod
- yesod-auth
- yesod-core
- yesod-form
- yesod-persistent
flags:
library-only:
@@ -129,56 +129,56 @@ flags:
library:
source-dirs: src
when:
- condition: (flag(dev)) || (flag(library-only))
then:
cpp-options: -DDEVELOPMENT
ghc-options:
- -Wall
- -Wunused-packages
- -fwarn-tabs
- -O0
- -fdefer-typed-holes
else:
ghc-options:
- -Wall
- -Wunused-packages
- -fwarn-tabs
- -O2
- -fdefer-typed-holes
- condition: (flag(disable-auth))
cpp-options: -DDISABLE_AUTH
- condition: (flag(dev)) || (flag(library-only))
then:
cpp-options: -DDEVELOPMENT
ghc-options:
- -Wall
- -Wunused-packages
- -fwarn-tabs
- -O0
- -fdefer-typed-holes
else:
ghc-options:
- -Wall
- -Wunused-packages
- -fwarn-tabs
- -O2
- -fdefer-typed-holes
- condition: (flag(disable-auth))
cpp-options: -DDISABLE_AUTH
tests:
agent-test:
source-dirs: test
main: Main.hs
ghc-options:
- -Wall
- -fdefer-typed-holes
- -Wall
- -fdefer-typed-holes
dependencies:
- ambassador-agent
- hspec >=2.0.0
- hspec-expectations
- hedgehog
- yesod-test
- random
- ambassador-agent
- hspec >=2.0.0
- hspec-expectations
- hedgehog
- yesod-test
- random
when:
- condition: false
other-modules: Paths_ambassador_agent
- condition: false
other-modules: Paths_ambassador_agent
executables:
agent:
source-dirs: app
main: main.hs
ghc-options:
- -Wall
- -threaded
- -rtsopts
- -with-rtsopts=-N
- -fdefer-typed-holes
- -Wall
- -threaded
- -rtsopts
- -with-rtsopts=-N
- -fdefer-typed-holes
dependencies:
- ambassador-agent
- ambassador-agent
when:
- buildable: false
condition: flag(library-only)
- condition: false
other-modules: Paths_ambassador_agent
- buildable: false
condition: flag(library-only)
- condition: false
other-modules: Paths_ambassador_agent

View File

@@ -186,6 +186,8 @@ cutoffDuringUpdate m = do
path <- asks $ pathInfo . reqWaiRequest . handlerRequest
case path of
[v] | v == "v" <> (show . major $ agentVersion) -> m
[auth] | auth == "auth" -> m
(_:ssh:_) | ssh == "sshKeys" -> m
_ -> handleS9ErrT $ throwE UpdateInProgressE
Nothing -> m

View File

@@ -109,7 +109,11 @@ type AllEffects m
( Labelled
"databaseConnection"
(ReaderT ConnectionPool)
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
( Labelled
"lanThread"
(ReaderT (MVar ThreadId))
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
)
)
)
)
@@ -122,6 +126,8 @@ intoHandler m = do
runM
. handleS9ErrC
. flip runReaderT ctx
. flip runReaderT (appLanThread ctx)
. runLabelled @"lanThread"
. flip runReaderT (appConnPool ctx)
. runLabelled @"databaseConnection"
. flip runReaderT fsbase
@@ -376,6 +382,7 @@ postUninstallAppLogic :: ( HasFilesystemBase sig m
, MonadIO m
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
)
=> AppId
-> AppMgr2.DryRun
@@ -413,6 +420,7 @@ postInstallNewAppR appId = do
postInstallNewAppLogic :: forall sig m a
. ( Has (Reader AgentCtx) sig m
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
, Has (Error S9Error) sig m

View File

@@ -7,11 +7,11 @@ import Startlude hiding ( Reader
, runReader
)
import Control.Effect.Labelled hiding ( Handler )
import Control.Effect.Reader.Labelled
import Control.Carrier.Error.Church
import Control.Carrier.Lift
import Control.Carrier.Reader ( runReader )
import Control.Effect.Labelled hiding ( Handler )
import Control.Effect.Reader.Labelled
import Data.Aeson
import qualified Data.HashMap.Strict as HM
import Data.UUID.V4
@@ -20,8 +20,13 @@ import Yesod.Auth
import Yesod.Core
import Yesod.Core.Types
import Control.Concurrent.STM
import Exinst
import Foundation
import Handler.Network
import Handler.Util
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Background
import Lib.Error
import qualified Lib.External.AppMgr as AppMgr
import qualified Lib.Notifications as Notifications
@@ -29,10 +34,6 @@ import Lib.Password
import Lib.Types.Core
import Lib.Types.Emver
import Model
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Background
import Control.Concurrent.STM
import Exinst
data CreateBackupReq = CreateBackupReq
@@ -58,8 +59,9 @@ instance FromJSON RestoreBackupReq where
pure RestoreBackupReq { .. }
data EjectDiskReq = EjectDiskReq
{ ejectDiskLogicalName :: Text
} deriving (Eq, Show)
{ ejectDiskLogicalName :: Text
}
deriving (Eq, Show)
instance FromJSON EjectDiskReq where
parseJSON = withObject "Eject Disk Req" $ \o -> do
ejectDiskLogicalName <- o .: "logicalName"
@@ -100,6 +102,8 @@ postRestoreBackupR appId = disableEndpointOnFailedUpdate $ do
& runReader appConnPool
& runLabelled @"backgroundJobCache"
& runReader appBackgroundJobs
& runLabelled @"lanThread"
& runReader appLanThread
& handleS9ErrC
& runM
@@ -173,6 +177,7 @@ stopBackupLogic appId = do
restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
, Has (Error S9Error) sig m
, Has AppMgr2.AppMgr sig m
, MonadIO m
@@ -181,10 +186,11 @@ restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)
-> RestoreBackupReq
-> m ()
restoreBackupLogic appId RestoreBackupReq {..} = do
jobCache <- ask @"backgroundJobCache"
db <- ask @"databaseConnection"
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
(show appId)
lanThread <- ask @"lanThread"
jobCache <- ask @"backgroundJobCache"
db <- ask @"databaseConnection"
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
(show appId)
res <- liftIO . atomically $ do
(JobCache jobs) <- readTVar jobCache
case HM.lookup appId jobs of
@@ -206,10 +212,13 @@ restoreBackupLogic appId RestoreBackupReq {..} = do
let notif = case appmgrRes of
Left e -> Notifications.RestoreFailed e
Right _ -> Notifications.RestoreSucceeded
resetRes <- runExceptT @S9Error $ runReader lanThread . runLabelled @"lanThread" $ postResetLanLogic
case resetRes of
Left _ -> pure () -- temporarily forbidden is the only possible thing here so ignore it
Right () -> pure ()
flip runSqlPool db $ void $ Notifications.emit appId version notif
liftIO . atomically $ modifyTVar jobCache (insertJob appId Restore tid)
listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo]
listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither

View File

@@ -1,16 +1,19 @@
module Handler.Network where
import Startlude hiding ( Reader
, ask
, asks
, runReader
)
import Control.Carrier.Lift ( runM )
import Control.Effect.Error
import Control.Carrier.Reader
import Lib.Error
import Yesod.Core ( getYesod )
import Control.Carrier.Reader ( runReader )
import Control.Effect.Labelled ( runLabelled )
import Control.Effect.Reader.Labelled
import Foundation
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Types.Core
@@ -18,11 +21,12 @@ import Lib.Types.Core
postResetLanR :: Handler ()
postResetLanR = do
ctx <- getYesod
runM . handleS9ErrC . runReader ctx $ postResetLanLogic
runM . handleS9ErrC . runReader (appLanThread ctx) . runLabelled @"lanThread" $ postResetLanLogic
postResetLanLogic :: (MonadIO m, Has (Reader AgentCtx) sig m, Has (Error S9Error) sig m) => m ()
postResetLanLogic :: (MonadIO m, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m, Has (Error S9Error) sig m)
=> m ()
postResetLanLogic = do
threadVar <- asks appLanThread
threadVar <- ask @"lanThread"
mtid <- liftIO . tryTakeMVar $ threadVar
case mtid of
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"

View File

@@ -11,8 +11,7 @@ module Lib.Algebra.Domain.AppMgr
( module Lib.Algebra.Domain.AppMgr
, module Lib.Algebra.Domain.AppMgr.Types
, module Lib.Algebra.Domain.AppMgr.TH
)
where
) where
import Startlude
@@ -26,31 +25,31 @@ import Data.Singletons.Prelude hiding ( Error )
import Data.Singletons.Prelude.Either
import qualified Data.String as String
import Lib.Algebra.Domain.AppMgr.Types
import Control.Monad.Base ( MonadBase(..) )
import Control.Monad.Fail ( MonadFail(fail) )
import Control.Monad.Trans.Class ( MonadTrans )
import Control.Monad.Trans.Control ( MonadBaseControl(..)
, MonadTransControl(..)
, defaultLiftBaseWith
, defaultRestoreM
)
import Control.Monad.Trans.Resource ( MonadResource(..) )
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Lazy as LBS
import Data.String.Interpolate.IsString
( i )
import Lib.Algebra.Domain.AppMgr.TH
import Lib.Algebra.Domain.AppMgr.Types
import Lib.Error
import qualified Lib.External.AppManifest as Manifest
import Lib.TyFam.ConditionalData
import Lib.Types.Core ( AppId(..)
, AppContainerStatus(..)
import Lib.Types.Core ( AppContainerStatus(..)
, AppId(..)
)
import Lib.Types.NetAddress
import Lib.Types.Emver
import Control.Monad.Trans.Class ( MonadTrans )
import qualified Data.ByteString.Lazy as LBS
import System.Process.Typed
import Data.String.Interpolate.IsString
( i )
import Control.Monad.Base ( MonadBase(..) )
import Control.Monad.Fail ( MonadFail(fail) )
import Control.Monad.Trans.Resource ( MonadResource(..) )
import Control.Monad.Trans.Control ( defaultLiftBaseWith
, defaultRestoreM
, MonadTransControl(..)
, MonadBaseControl(..)
)
import qualified Data.ByteString.Char8 as C8
import Lib.Types.NetAddress
import System.Process
import System.Process.Typed
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
@@ -371,13 +370,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
(L (List (SRight flags))) -> do
let renderedFlags = (genInclusiveFlag <$> fromSing flags) <> ["--json"]
let args = "list" : renderedFlags
(ec, out) <- readProcessInheritStderr "appmgr" args ""
res <- case ec of
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
Right x -> pure x
ExitFailure n -> throwError $ AppMgrE "list" n
pure $ ctx $> res
let runIt retryCount = do
(ec, out) <- readProcessInheritStderr "appmgr" args ""
case ec of
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
Right x -> pure $ ctx $> x
ExitFailure 6 ->
if retryCount > 0 then runIt (retryCount - 1) else throwError $ AppMgrE "list" 6
ExitFailure n -> throwError $ AppMgrE "list" n
runIt (1 :: Word) -- with 1 retry
(L (Remove dryorpurge appId)) -> do
let args = "remove" : case dryorpurge of
Left (DryRun True) -> ["--dry-run", show appId, "--json"]

View File

@@ -48,6 +48,7 @@ import System.Process ( callCommand )
import Constants
import Control.Effect.Error hiding ( run )
import Control.Effect.Labelled ( runLabelled )
import Daemon.ZeroConf ( getStart9AgentHostname )
import qualified Data.Text as T
import Foundation
@@ -97,12 +98,12 @@ parseKernelVersion = do
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
synchronizer :: Synchronizer
synchronizer = sync_0_2_9
synchronizer = sync_0_2_11
{-# INLINE synchronizer #-}
sync_0_2_9 :: Synchronizer
sync_0_2_9 = Synchronizer
"0.2.9"
sync_0_2_11 :: Synchronizer
sync_0_2_11 = Synchronizer
"0.2.11"
[ syncCreateAgentTmp
, syncCreateSshDir
, syncRemoveAvahiSystemdDependency
@@ -125,6 +126,8 @@ sync_0_2_9 = Synchronizer
, syncConvertEcdsaCerts
, syncRestarterService
, syncInstallEject
, syncDropCertificateUniqueness
, syncRemoveDefaultNginxCfg
]
syncCreateAgentTmp :: SyncOp
@@ -436,10 +439,11 @@ syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False
Left _ -> pure True
Right v -> not . (v <||) <$> asks (appMgrVersionSpec . appSettings)
migrate = fmap (either absurd id) . runExceptT . flip catchE failUpdate $ do
lan <- asks appLanThread
avs <- asks $ appMgrVersionSpec . appSettings
av <- AppMgr.installNewAppMgr avs
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
flip runReaderT lan $ runLabelled @"lanThread" $ postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
syncUpgradeLifeline :: SyncOp
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
@@ -581,11 +585,11 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
liftIO $ callCommand "systemctl enable restarter.timer"
syncUpgradeTor :: SyncOp
syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
syncUpgradeTor = SyncOp "Install Tor 0.3.5.14-1" check migrate False
where
check =
liftIO
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.12-1|] $| conduit await)
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.14-1|] $| conduit await)
$> False
)
`catch` \(e :: ProcessException) -> case e of
@@ -593,7 +597,40 @@ syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
_ -> throwIO e
migrate = liftIO . run $ do
shell "apt-get update"
shell "apt-get install -y tor=0.3.5.12-1"
shell "apt-get install -y tor=0.3.5.14-1"
syncDropCertificateUniqueness :: SyncOp
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False
where
uni = "unique_subject = no\n"
check = do
base <- asks $ appFilesystemBase . appSettings
contentsRoot <-
liftIO
$ (fmap Just . BS.readFile . toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base)
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
contentsInt <-
liftIO
$ (fmap Just . BS.readFile . toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base)
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
case (contentsRoot, contentsInt) of
(Just root, Just int) -> pure $ uni /= root || uni /= int
_ -> pure True
migrate = do
base <- asks $ appFilesystemBase . appSettings
liftIO $ BS.writeFile (toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base) uni
liftIO $ BS.writeFile (toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base) uni
syncRemoveDefaultNginxCfg :: SyncOp
syncRemoveDefaultNginxCfg = SyncOp "Remove Default Nginx Configuration" check migrate False
where
check = do
base <- asks $ appFilesystemBase . appSettings
liftIO $ doesPathExist (toS $ nginxSitesEnabled "default" `relativeTo` base)
migrate = do
base <- asks $ appFilesystemBase . appSettings
liftIO $ removeFileIfExists (toS $ nginxSitesEnabled "default" `relativeTo` base)
liftIO $ systemCtl RestartService "nginx" $> ()
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
failUpdate e = do

1
appmgr/.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
**/*.rs.bk
.DS_Store

2
appmgr/Cargo.lock generated
View File

@@ -41,7 +41,7 @@ checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
[[package]]
name = "appmgr"
version = "0.2.9"
version = "0.2.11"
dependencies = [
"async-trait",
"avahi-sys",

View File

@@ -2,7 +2,7 @@
authors = ["Aiden McClelland <me@drbonez.dev>"]
edition = "2018"
name = "appmgr"
version = "0.2.9"
version = "0.2.11"
[lib]
name = "appmgrlib"

View File

@@ -3,6 +3,11 @@
set -e
shopt -s expand_aliases
if [ "$0" != "./build-dev.sh" ]; then
>&2 echo "Must be run from appmgr directory"
exit 1
fi
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
cd ..

15
appmgr/build-portable.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -e
shopt -s expand_aliases
if [ "$0" != "./build-portable.sh" ]; then
>&2 echo "Must be run from appmgr directory"
exit 1
fi
alias 'rust-musl-builder'='docker run --rm -it -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl'
cd ..
rust-musl-builder sh -c "(cd appmgr && cargo build --release --target=x86_64-unknown-linux-musl --features=portable,production --no-default-features)"
cd appmgr

View File

@@ -3,6 +3,11 @@
set -e
shopt -s expand_aliases
if [ "$0" != "./build-prod.sh" ]; then
>&2 echo "Must be run from appmgr directory"
exit 1
fi
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
cd ..

View File

@@ -1,3 +1,4 @@
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use argon2::Config;
@@ -10,6 +11,7 @@ use serde::Serialize;
use crate::util::from_yaml_async_reader;
use crate::util::to_yaml_async_writer;
use crate::util::Invoke;
use crate::util::PersistencePath;
use crate::version::VersionT;
use crate::Error;
use crate::ResultExt;
@@ -224,6 +226,28 @@ pub async fn restore_backup<P: AsRef<Path>>(
}
crate::tor::restart().await?;
// Delete the fullchain certificate, so it can be regenerated with the restored tor pubkey address
PersistencePath::from_ref("apps")
.join(&app_id)
.join("cert-local.fullchain.crt.pem")
.delete()
.await?;
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}

View File

@@ -24,7 +24,6 @@ pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> {
if status == crate::apps::DockerStatus::Stopped {
if update_metadata {
crate::config::configure(name, None, None, false).await?;
crate::dependencies::update_shared(name).await?;
crate::dependencies::update_binds(name).await?;
}
crate::apps::set_needs_restart(name, false).await?;

View File

@@ -188,31 +188,6 @@ pub async fn auto_configure(
crate::config::configure(dependency, Some(dependency_config), None, dry_run).await
}
pub async fn update_shared(dependency_id: &str) -> Result<(), Error> {
let dependency_manifest = crate::apps::manifest(dependency_id).await?;
if let Some(shared) = dependency_manifest.shared {
for dependent_id in &crate::apps::dependents(dependency_id, false).await? {
let dependent_manifest = crate::apps::manifest(&dependent_id).await?;
if dependent_manifest
.dependencies
.0
.get(dependency_id)
.ok_or_else(|| failure::format_err!("failed to index dependent: {}", dependent_id))?
.mount_shared
{
tokio::fs::create_dir_all(
Path::new(crate::VOLUMES)
.join(dependency_id)
.join(&shared)
.join(&dependent_id),
)
.await?;
}
}
}
Ok(())
}
pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
let dependent_manifest = crate::apps::manifest(dependent_id).await?;
let dependency_manifests = futures::future::try_join_all(
@@ -222,12 +197,19 @@ pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
.into_iter()
.filter(|(_, info)| info.mount_public || info.mount_shared)
.map(|(id, info)| async {
crate::apps::manifest(&id).await.map(|man| (id, info, man))
Ok::<_, Error>(if crate::apps::list_info().await?.contains_key(&id) {
let man = crate::apps::manifest(&id).await?;
Some((id, info, man))
} else {
None
})
}),
)
.await?;
// i just have a gut feeling this shouldn't be concurrent
for (dependency_id, info, dependency_manifest) in dependency_manifests {
for (dependency_id, info, dependency_manifest) in
dependency_manifests.into_iter().filter_map(|a| a)
{
match (dependency_manifest.public, info.mount_public) {
(Some(public), true) => {
let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public);

View File

@@ -1,10 +1,11 @@
use std::path::Path;
use failure::ResultExt as _;
use futures::future::try_join_all;
use crate::util::Invoke;
use crate::Error;
use crate::ResultExt;
use crate::ResultExt as _;
pub const FSTAB: &'static str = "/etc/fstab";
@@ -153,6 +154,11 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
dst: P1,
read_only: bool,
) -> Result<(), Error> {
log::info!(
"Binding {} to {}",
src.as_ref().display(),
dst.as_ref().display()
);
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(dst.as_ref())
.stdout(std::process::Stdio::null())
@@ -185,6 +191,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
}
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
log::info!("Unmounting {}.", mount_point.as_ref().display());
let umount_output = tokio::process::Command::new("umount")
.arg(mount_point.as_ref())
.output()
@@ -192,10 +199,14 @@ pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
crate::ensure_code!(
umount_output.status.success(),
crate::error::FILESYSTEM_ERROR,
"Error Unmounting Drive: {}",
"Error Unmounting Drive: {}: {}",
mount_point.as_ref().display(),
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
);
tokio::fs::remove_dir_all(mount_point.as_ref()).await?;
tokio::fs::remove_dir_all(mount_point.as_ref())
.await
.with_context(|e| format!("rm {}: {}", mount_point.as_ref().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(())
}

View File

@@ -561,14 +561,17 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
crate::config::configure(&manifest.id, Some(empty_config), None, false).await?;
}
}
crate::dependencies::update_binds(&manifest.id).await?;
for (dep_id, dep_info) in manifest.dependencies.0 {
if dep_info.mount_shared
&& crate::apps::list_info().await?.get(&dep_id).is_some()
&& crate::apps::manifest(&dep_id).await?.shared.is_some()
&& crate::apps::status(&dep_id, false).await?.status
!= crate::apps::DockerStatus::Stopped
{
crate::apps::set_needs_restart(&dep_id, true).await?;
match crate::apps::status(&dep_id, false).await?.status {
crate::apps::DockerStatus::Stopped => (),
crate::apps::DockerStatus::Running => crate::control::restart_app(&dep_id).await?,
_ => crate::apps::set_needs_restart(&dep_id, true).await?,
}
}
}

View File

@@ -9,6 +9,7 @@ server {{
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0;
}}
}}
server {{

View File

@@ -4,5 +4,6 @@ server {{
location / {{
proxy_pass http://{app_ip}:{internal_port}/;
proxy_set_header Host $host;
client_max_body_size 0;
}}
}}

View File

@@ -1,9 +1,11 @@
use crate::failure::ResultExt;
use std::path::Path;
use linear_map::LinearMap;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::Error;
use crate::ResultExt as _;
pub async fn remove(
name: &str,
@@ -55,48 +57,79 @@ pub async fn remove(
log::info!("Removing tor hidden service.");
crate::tor::rm_svc(name).await?;
log::info!("Removing app metadata.");
tokio::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))
.await?;
log::info!("Destroying mounted volume.");
let metadata_path = Path::new(crate::PERSISTENCE_DIR).join("apps").join(name);
tokio::fs::remove_dir_all(&metadata_path)
.await
.with_context(|e| format!("rm {}: {}", metadata_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
log::info!("Unbinding shared filesystem.");
for (dep, info) in manifest.dependencies.0.iter() {
if info.mount_public {
crate::disks::unmount(
Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("public")
.join(&dep),
)
.await?;
let installed_apps = crate::apps::list_info().await?;
for (dep, _) in manifest.dependencies.0.iter() {
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("public")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
if info.mount_shared {
if let Some(shared) = match crate::apps::manifest(dep).await {
Ok(man) => man.shared,
Err(e) => {
log::error!("Failed to Fetch Dependency Manifest: {}", e);
None
}
} {
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("shared")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
}
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("shared")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
if installed_apps.contains_key(dep) {
let dep_man = crate::apps::manifest(dep).await?;
if let Some(shared) = dep_man.shared {
let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name);
if path.exists() {
tokio::fs::remove_dir_all(
Path::new(crate::VOLUMES).join(dep).join(&shared).join(name),
)
.await?;
tokio::fs::remove_dir_all(&path)
.await
.with_context(|e| format!("rm {}: {}", path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
}
} else {
log::warn!("{} is not installed, skipping...", dep);
}
}
if manifest.public.is_some() || manifest.shared.is_some() {
for dependent in crate::apps::dependents(name, false).await? {
let path = Path::new(crate::VOLUMES)
.join(&dependent)
.join("start9")
.join("public")
.join(name);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
let path = Path::new(crate::VOLUMES)
.join(dependent)
.join("start9")
.join("shared")
.join(name);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
}
}
tokio::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name)).await?;
log::info!("Destroying mounted volume.");
let volume_path = Path::new(crate::VOLUMES).join(name);
tokio::fs::remove_dir_all(&volume_path)
.await
.with_context(|e| format!("rm {}: {}", volume_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
log::info!("Pruning unused docker images.");
crate::ensure_code!(
std::process::Command::new("docker")

View File

@@ -246,7 +246,14 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err
log::info!("Writing LAN certificates for {}", app_id);
let base_path = PersistencePath::from_ref("apps").join(&app_id);
let key_path = base_path.join("cert-local.key.pem").path();
if tokio::fs::metadata(&key_path).await.is_err() {
let conf_path = base_path.join("cert-local.csr.conf").path();
let req_path = base_path.join("cert-local.csr").path();
let cert_path = base_path.join("cert-local.crt.pem").path();
let fullchain_path = base_path.join("cert-local.fullchain.crt.pem");
if !fullchain_path.exists().await
|| tokio::fs::metadata(&key_path).await.is_err()
{
let mut fullchain_file = fullchain_path.write(None).await?;
tokio::process::Command::new("openssl")
.arg("ecparam")
.arg("-genkey")
@@ -257,9 +264,6 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err
.arg(&key_path)
.invoke("OpenSSL GenKey")
.await?;
}
let conf_path = base_path.join("cert-local.csr.conf").path();
if tokio::fs::metadata(&conf_path).await.is_err() {
tokio::fs::write(
&conf_path,
format!(
@@ -268,9 +272,6 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err
),
)
.await?;
}
let req_path = base_path.join("cert-local.csr").path();
if tokio::fs::metadata(&req_path).await.is_err() {
tokio::process::Command::new("openssl")
.arg("req")
.arg("-config")
@@ -287,9 +288,6 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err
.arg(&req_path)
.invoke("OpenSSL Req")
.await?;
}
let cert_path = base_path.join("cert-local.crt.pem").path();
if tokio::fs::metadata(&cert_path).await.is_err() {
tokio::process::Command::new("openssl")
.arg("ca")
.arg("-batch")
@@ -311,11 +309,7 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err
.arg(&cert_path)
.invoke("OpenSSL CA")
.await?;
}
let fullchain_path = base_path.join("cert-local.fullchain.crt.pem");
if !fullchain_path.exists().await {
log::info!("Writing fullchain to: {}", fullchain_path.path().display());
let mut fullchain_file = fullchain_path.write(None).await?;
tokio::io::copy(
&mut tokio::fs::File::open(&cert_path).await?,
&mut *fullchain_file,

View File

@@ -110,6 +110,14 @@ impl PersistencePath {
pub async fn for_update(self) -> Result<UpdateHandle<ForRead>, Error> {
UpdateHandle::new(self).await
}
pub async fn delete(&self) -> Result<(), Error> {
match tokio::fs::remove_file(self.path()).await {
Ok(()) => Ok(()),
Err(k) if k.kind() == std::io::ErrorKind::NotFound => Ok(()),
e => e.with_code(crate::error::FILESYSTEM_ERROR),
}
}
}
#[derive(Debug)]

View File

@@ -26,7 +26,10 @@ mod v0_2_7;
mod v0_2_8;
mod v0_2_9;
pub use v0_2_9::Version as Current;
mod v0_2_10;
mod v0_2_11;
pub use v0_2_11::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
@@ -48,6 +51,8 @@ enum Version {
V0_2_7(Wrapper<v0_2_7::Version>),
V0_2_8(Wrapper<v0_2_8::Version>),
V0_2_9(Wrapper<v0_2_9::Version>),
V0_2_10(Wrapper<v0_2_10::Version>),
V0_2_11(Wrapper<v0_2_11::Version>),
Other(emver::Version),
}
@@ -159,6 +164,8 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
}
@@ -249,6 +256,8 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
};

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_10: emver::Version = emver::Version::new(0, 2, 10, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_9::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_10
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,38 @@
use super::*;
use std::os::unix::process::ExitStatusExt;
const V0_2_11: emver::Version = emver::Version::new(0, 2, 11, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_10::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_11
}
async fn up(&self) -> Result<(), Error> {
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
manifest-version: 0
app-id: start9-ambassador
app-version: 0.2.9
app-version: 0.2.11
uri-rewrites:
- =/api -> http://{{start9-ambassador}}:5959/authenticate
- /api/ -> http://{{start9-ambassador}}:5959/

17415
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "embassy-ui",
"version": "0.2.9",
"version": "0.2.11",
"description": "GUI for EmbassyOS",
"author": "Start9 Labs",
"homepage": "https://github.com/Start9Labs/embassy-ui",

View File

@@ -298,11 +298,11 @@ export class ConfigCursor<T extends ValueType> {
const mappedCfg = this.mappedConfig()
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
const spec = this.spec()
let allKeys
let allKeys: Set<string>
if (spec.type === 'union') {
let unionSpec = spec as ValueSpecOf<'union'>
const labelForSelection = unionSpec.tag.id
allKeys = new Set([...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
} else {
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
}

View File

@@ -17,8 +17,15 @@
<ion-item-group>
<ion-item-divider></ion-item-divider>
<ion-item>
<ion-icon size="small" slot="start" *ngIf="!edited"
style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);"
name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="edited" style="margin-right: 15px" color="primary" name="ellipse">
</ion-icon>
<ion-label>{{ spec.tag.name }}</ion-label>
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One" [(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]" (ngModelChange)="handleUnionChange()">
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One"
[(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]"
(ngModelChange)="handleUnionChange()">
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
{{ spec.tag.variantNames[option.key] }}
<span *ngIf="option.key === spec.default"> (default)</span>
@@ -28,4 +35,4 @@
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
</ion-item-group>
</ion-content>
</ion-content>

View File

@@ -19,6 +19,7 @@ export class AppConfigUnionPage {
spec: ValueSpecUnion
value: object
error: string
edited: boolean
constructor (
private readonly modalCtrl: ModalController,
@@ -28,6 +29,7 @@ export class AppConfigUnionPage {
this.spec = this.cursor.spec()
this.value = this.cursor.config()
this.error = this.cursor.checkInvalid()
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
}
async dismiss () {
@@ -37,6 +39,8 @@ export class AppConfigUnionPage {
async handleUnionChange () {
this.value = mapUnionSpec(this.spec, this.value)
this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations()
this.error = this.cursor.checkInvalid()
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
}
setSelectOptions () {

View File

@@ -1,7 +1,7 @@
<ion-header>
<ion-toolbar>
<ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.9!</ion-label>
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.11!</ion-label>
</ion-title>
</ion-toolbar>
</ion-header>
@@ -9,10 +9,18 @@
<ion-content class="ion-padding">
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<h2>Highlights</h2>
<p class="main-content">
0.2.9 introduces LAN support for services running on the Embassy. A service's LAN address (.local URL) can be accessed while connected to the same network.
This is useful for two reasons: (1) LAN connections are significantly faster than Tor, and (2) if the Tor network is experiencing connectivity issues, you will not be locked out of your services.
</p>
<div class="main-content">
<p>This release includes several bugfixes to resolve:</p>
<ol>
<li>Refreshing error messages during configuration changes</li>
<li>Starting services with uninstalled optional dependencies</li>
<li>Uninstalling services with optional dependencies</li>
<li>Redirecting to HTTPS when navigating to LAN address</li>
<li>Displaying warning messages during concurrent upgrades of dependent services</li>
<li>Allowing larger file uploads</li>
<li>Patching a security fix for Tor</li>
</ol>
</div>
<div class="close-button">
<ion-button fill="outline" (click)="dismiss()">

View File

@@ -36,7 +36,7 @@
</ion-label>
</ion-item>
<ion-item>
<ion-button slot="start" fill="clear" color="primary">View Instructions</ion-button>
<ion-button slot="start" fill="clear" color="primary" (click)="viewInstructions()">View Instructions</ion-button>
</ion-item>
<ng-container *ngIf="!lanDisabled">

View File

@@ -12,8 +12,8 @@ import { ApiService } from 'src/app/services/api/api.service'
styleUrls: ['./lan.page.scss'],
})
export class LANPage {
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manual/general/secure-lan'
lanDocs = 'docs.start9labs.com/user-manual/general/secure-lan'
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manual/general/lan-setup'
lanDocs = 'docs.start9labs.com/user-manual/general/lan-setup'
lanAddress: string
fullDocumentationLink: string
@@ -60,6 +60,14 @@ export class LANPage {
})
}
viewInstructions (): void {
if (this.config.isConsulate) {
this.copyInstructions()
} else {
window.open(this.fullDocumentationLink, '_blank')
}
}
async copyLAN (): Promise <void> {
const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy')
@@ -72,9 +80,9 @@ export class LANPage {
await toast.present()
}
async copyDocumentation (): Promise < void > {
async copyInstructions (): Promise < void > {
const message = await copyToClipboard(this.fullDocumentationLink).then(
success => success ? 'copied documentation link to clipboard!' : 'failed to copy',
success => success ? 'copied link to clipboard!' : 'failed to copy',
)
const toast = await this.toastCtrl.create({

View File

@@ -492,8 +492,8 @@ const mockApiNotifications: ReqRes.GetNotificationsRes = [
const mockApiServer: () => ReqRes.GetServerRes = () => ({
serverId: 'start9-mockxyzab',
name: 'Embassy:12345678',
versionInstalled: '0.2.9',
versionLatest: '0.2.10',
versionInstalled: '0.2.11',
versionLatest: '0.2.12',
status: ServerStatus.RUNNING,
alternativeRegistryUrl: 'beta-registry.start9labs.com',
welcomeAck: true,

View File

@@ -1,5 +1,4 @@
{
"useMocks": false,
"mockOver": "tor",
"skipStartupAlerts": false
}