diff --git a/cmd/debug.go b/cmd/debug.go index ba54ec65f..df3e960fc 100644 --- a/cmd/debug.go +++ b/cmd/debug.go @@ -100,6 +100,9 @@ var specCmd = &cobra.Command{ batchRequirements, err := proj.CollectBatchRequirements() tui.CheckErr(err) + websiteRequirements, err := proj.CollectWebsiteRequirements() + tui.CheckErr(err) + additionalEnvFiles := []string{} if debugEnvFile != "" { @@ -115,7 +118,7 @@ var specCmd = &cobra.Command{ envVariables = map[string]string{} } - spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements) + spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements, websiteRequirements) tui.CheckErr(err) migrationImageContexts, err := collector.GetMigrationImageBuildContexts(serviceRequirements, batchRequirements, fs) diff --git a/cmd/run.go b/cmd/run.go index b690e59b6..6cbe38618 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -149,6 +149,23 @@ var runCmd = &cobra.Command{ tui.CheckErr(err) } + websiteBuildUpdates, err := proj.BuildWebsites(loadEnv) + tui.CheckErr(err) + + if isNonInteractive() { + fmt.Println("building project websites") + for update := range websiteBuildUpdates { + for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") { + fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line) + } + } + } else { + prog := teax.NewProgram(build.NewModel(websiteBuildUpdates, "Building Websites")) + // blocks but quits once the above updates channel is closed by the build process + _, err = prog.Run() + tui.CheckErr(err) + } + // Run the app code (project services) stopChan := make(chan bool) updatesChan := make(chan project.ServiceRunUpdate) @@ -179,6 +196,15 @@ var runCmd = &cobra.Command{ } }() + go func() { + err := proj.RunWebsites(localCloud) + if err != nil { + localCloud.Stop() + + tui.CheckErr(err) + } + }() + tui.CheckErr(err) // FIXME: This is a hack to get labelled logs into the TUI // We should refactor the system logs to be more generic diff --git a/cmd/stack.go b/cmd/stack.go index 276394605..6cfbaaed1 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -220,6 +220,9 @@ var stackUpdateCmd = &cobra.Command{ batchRequirements, err := proj.CollectBatchRequirements() tui.CheckErr(err) + websiteRequirements, err := proj.CollectWebsiteRequirements() + tui.CheckErr(err) + additionalEnvFiles := []string{} if envFile != "" { @@ -240,13 +243,13 @@ var stackUpdateCmd = &cobra.Command{ envVariables["NITRIC_BETA_PROVIDERS"] = "true" } - spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements) + spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements, websiteRequirements) tui.CheckErr(err) migrationImageContexts, err := collector.GetMigrationImageBuildContexts(serviceRequirements, batchRequirements, fs) tui.CheckErr(err) - // Build images from contexts and provide updates on the builds + // Build images from contexts and provide updates on the builds if len(migrationImageContexts) > 0 { migrationBuildUpdates, err := project.BuildMigrationImages(fs, migrationImageContexts, !noBuilder) tui.CheckErr(err) @@ -274,6 +277,23 @@ var stackUpdateCmd = &cobra.Command{ } } + websiteBuildUpdates, err := proj.BuildWebsites(envVariables) + tui.CheckErr(err) + + if isNonInteractive() { + fmt.Println("building project websites") + for update := range websiteBuildUpdates { + for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") { + fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line) + } + } + } else { + prog := teax.NewProgram(build.NewModel(websiteBuildUpdates, "Building Websites")) + // blocks but quits once the above updates channel is closed by the build process + _, err = prog.Run() + tui.CheckErr(err) + } + providerStdout := make(chan string) // Step 4. Start the deployment provider server diff --git a/cmd/start.go b/cmd/start.go index 9de383df8..24097455b 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -238,6 +238,16 @@ var startCmd = &cobra.Command{ } }() + // FIXME: Duplicate code + go func() { + err := proj.RunWebsitesWithCommand(localCloud, stopChan, updatesChan, localEnv) + if err != nil { + localCloud.Stop() + + tui.CheckErr(err) + } + }() + // FIXME: This is a hack to get labelled logs into the TUI // We should refactor the system logs to be more generic systemChan := make(chan project.ServiceRunUpdate) diff --git a/go.mod b/go.mod index 35254abf2..3e02fb68c 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ replace github.com/mattn/go-ieproxy => github.com/darthShadow/go-ieproxy v0.0.0- require github.com/golangci/golangci-lint v1.61.0 require ( - github.com/AlecAivazis/survey/v2 v2.3.6 github.com/asdine/storm v2.1.2+incompatible github.com/aws/aws-sdk-go v1.44.175 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -23,12 +22,12 @@ require ( github.com/hashicorp/consul/sdk v0.13.0 github.com/hashicorp/go-getter v1.6.2 github.com/hashicorp/go-version v1.7.0 - github.com/nitrictech/nitric/core v0.0.0-20241003062412-76ea6275fb0b + github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.1 github.com/valyala/fasthttp v1.55.0 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect - golang.org/x/mod v0.21.0 // indirect + golang.org/x/mod v0.22.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect google.golang.org/grpc v1.66.0 gopkg.in/yaml.v2 v2.4.0 @@ -52,11 +51,12 @@ require ( github.com/olahol/melody v1.1.3 github.com/robfig/cron/v3 v3.0.1 github.com/samber/lo v1.38.1 + github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.9.0 github.com/wk8/go-ordered-map/v2 v2.1.8 go.etcd.io/bbolt v1.3.6 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.10.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -115,7 +115,7 @@ require ( github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/firefart/nonamedreturns v1.0.5 // indirect @@ -173,7 +173,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/julz/importas v0.1.0 // indirect github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kisielk/errcheck v1.7.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect github.com/klauspost/compress v1.17.9 // indirect @@ -197,7 +196,6 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgechev/revive v1.3.9 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -238,7 +236,6 @@ require ( github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/securego/gosec/v2 v2.21.2 // indirect github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sivchari/tenv v1.10.0 // indirect github.com/sonatard/noctx v0.0.2 // indirect @@ -282,14 +279,14 @@ require ( go.uber.org/automaxprocs v1.5.3 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.27.0 // indirect google.golang.org/api v0.196.0 // indirect google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect diff --git a/go.sum b/go.sum index 13015e43c..f3aaaafb1 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,6 @@ github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8 github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY= github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM= -github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= -github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= @@ -79,14 +77,12 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= github.com/Sereal/Sereal v0.0.0-20221130110801-16a4f76670cd h1:rP6LH3aVJTIxgTA3q79sSfnt8DvOlt17IRAklRBN+xo= github.com/Sereal/Sereal v0.0.0-20221130110801-16a4f76670cd/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= -github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= -github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= @@ -179,8 +175,6 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= @@ -211,8 +205,8 @@ github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQt github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -431,8 +425,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -476,8 +468,6 @@ github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= @@ -533,7 +523,6 @@ github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859 github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -557,9 +546,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A= github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -599,8 +585,12 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b h1:wZeUrnmhYjdhSuL6ov+kVfuFJC9H14sk0kzEpt6aRoo= github.com/nitrictech/nitric/cloud/common v0.0.0-20241003062412-76ea6275fb0b/go.mod h1:ZsCdb3xbukhXAp9ZNbV6qWJqRC+eLkxhXy8bhs/cC2A= -github.com/nitrictech/nitric/core v0.0.0-20241003062412-76ea6275fb0b h1:ImQFk66gRM3v9A6qmPImOiV3HJMDAX93X5rplMKn6ok= -github.com/nitrictech/nitric/core v0.0.0-20241003062412-76ea6275fb0b/go.mod h1:9bQnYPqLzq8CcPk5MHT3phg19CWJhDlFOfdIv27lwwM= +github.com/nitrictech/nitric/core v0.0.0-20250123065014-599bda2a2582 h1:dKVFR/rquvNB/FNu8GFAZJ5RcW7HrgyE+I8rdmvKwYc= +github.com/nitrictech/nitric/core v0.0.0-20250123065014-599bda2a2582/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58= +github.com/nitrictech/nitric/core v0.0.0-20250123070044-4b31d7498e96 h1:GdRQEkMYrZehM2SKkUecR2/SPa5TuKpxuuZetShZnAI= +github.com/nitrictech/nitric/core v0.0.0-20250123070044-4b31d7498e96/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58= +github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae h1:hpSGt8KQ4OPPLqGvf5sFTv7h9isngWQLveVcfW4Z5i4= +github.com/nitrictech/nitric/core v0.0.0-20250123074029-0306df1e20ae/go.mod h1:3kPpyO2oZEGfurDVsTh9XTg43b/4JAIbLkaktEvRF58= github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= @@ -750,7 +740,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -866,8 +855,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -912,8 +901,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -955,8 +944,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -979,8 +968,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1031,7 +1020,6 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1042,17 +1030,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1063,8 +1050,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1132,8 +1119,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 90163fa1c..397fd3db4 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -36,6 +36,7 @@ import ( "github.com/nitrictech/cli/pkg/cloud/sql" "github.com/nitrictech/cli/pkg/cloud/storage" "github.com/nitrictech/cli/pkg/cloud/topics" + "github.com/nitrictech/cli/pkg/cloud/websites" "github.com/nitrictech/cli/pkg/cloud/websockets" "github.com/nitrictech/cli/pkg/grpcx" "github.com/nitrictech/cli/pkg/netx" @@ -67,6 +68,7 @@ type LocalCloud struct { Storage *storage.LocalStorageService Topics *topics.LocalTopicsAndSubscribersService Websockets *websockets.LocalWebsocketService + Websites *websites.LocalWebsiteService Queues *queues.LocalQueuesService Databases *sql.LocalSqlServer } @@ -315,6 +317,8 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { return nil, err } + localWebsites := websites.NewLocalWebsitesService(localGateway.GetApiAddress) + return &LocalCloud{ servers: make(map[string]*server.NitricServer), Apis: localApis, @@ -325,6 +329,7 @@ func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) { Storage: localStorage, Topics: localTopics, Websockets: localWebsockets, + Websites: localWebsites, Gateway: localGateway, Secrets: localSecrets, KeyValue: keyvalueService, diff --git a/pkg/cloud/websites/websites.go b/pkg/cloud/websites/websites.go new file mode 100644 index 000000000..5e8b8d772 --- /dev/null +++ b/pkg/cloud/websites/websites.go @@ -0,0 +1,196 @@ +// Copyright Nitric Pty Ltd. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package websites + +import ( + "fmt" + "maps" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/asaskevich/EventBus" + "github.com/nitrictech/cli/pkg/netx" + deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1" +) + +type WebsitePb = deploymentspb.Website + +type Website struct { + WebsitePb + + Name string +} + +type ( + WebsiteName = string + State = map[WebsiteName]string + GetApiAddress = func(apiName string) string +) + +type LocalWebsiteService struct { + websiteRegLock sync.RWMutex + state State + port int + getApiAddress GetApiAddress + + bus EventBus.Bus +} + +const localWebsitesTopic = "local_websites" + +func (l *LocalWebsiteService) publishState() { + l.bus.Publish(localWebsitesTopic, maps.Clone(l.state)) +} + +func (l *LocalWebsiteService) SubscribeToState(fn func(State)) { + // ignore the error, it's only returned if the fn param isn't a function + _ = l.bus.Subscribe(localWebsitesTopic, fn) +} + +// register - Register a new website +func (l *LocalWebsiteService) register(website Website) { + l.websiteRegLock.Lock() + defer l.websiteRegLock.Unlock() + + l.state[website.Name] = fmt.Sprintf("http://localhost:%d/%s", l.port, strings.TrimPrefix(website.BasePath, "/")) + + l.publishState() +} + +// deregister - Deregister a website +func (l *LocalWebsiteService) deregister(websiteName string) { + l.websiteRegLock.Lock() + defer l.websiteRegLock.Unlock() + + delete(l.state, websiteName) + + l.publishState() +} + +type staticSiteHandler struct { + website Website + port int +} + +// ServeHTTP - Serve a static website from the local filesystem +func (h staticSiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := filepath.Join(h.website.OutputDirectory, r.URL.Path) + + // check whether a file exists or is a directory at the given path + fi, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + // if the file doesn't exist, serve the error page with a 404 status code + http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.ErrorDocument)) + return + } + + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if fi.IsDir() { + http.ServeFile(w, r, filepath.Join(h.website.OutputDirectory, h.website.IndexDocument)) + return + } + + // otherwise, use http.FileServer to serve the static file + http.FileServer(http.Dir(h.website.OutputDirectory)).ServeHTTP(w, r) +} + +// Serve - Serve a website from the local filesystem +func (l *LocalWebsiteService) Start(websites []Website) error { + newLis, err := netx.GetNextListener(netx.MinPort(5000)) + if err != nil { + return err + } + + l.port = newLis.Addr().(*net.TCPAddr).Port + + _ = newLis.Close() + + // Initialize the multiplexer only if websites will be served + mux := http.NewServeMux() + + // Register the API handler + mux.HandleFunc("/api/{name}/", func(w http.ResponseWriter, r *http.Request) { + // get the api name from the request path + apiName := r.PathValue("name") + + // get the address of the api + apiAddress := l.getApiAddress(apiName) + if apiAddress == "" { + http.Error(w, fmt.Sprintf("api %s not found", apiName), http.StatusNotFound) + return + } + + // Strip /api/{name}/ from the URL path + newPath := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/api/%s", apiName)) + + // Target backend API server + target, _ := url.Parse(apiAddress) + + // Reverse proxy request + proxy := httputil.NewSingleHostReverseProxy(target) + r.URL.Path = newPath + + // Forward the modified request to the backend + proxy.ServeHTTP(w, r) + }) + + // Register the SPA handler for each website + for _, website := range websites { + spa := staticSiteHandler{website: website, port: l.port} + + if website.BasePath == "/" { + mux.Handle("/", spa) + } else { + mux.Handle(website.BasePath+"/", http.StripPrefix(website.BasePath+"/", spa)) + } + } + + // Start the server with the multiplexer + go func() { + addr := fmt.Sprintf(":%d", l.port) + if err := http.ListenAndServe(addr, mux); err != nil { + fmt.Printf("Failed to start server: %s\n", err) + } + }() + + // Register the websites + for _, website := range websites { + l.register(website) + } + + return nil +} + +func NewLocalWebsitesService(getApiAddress GetApiAddress) *LocalWebsiteService { + return &LocalWebsiteService{ + state: State{}, + bus: EventBus.New(), + getApiAddress: getApiAddress, + } +} diff --git a/pkg/collector/spec.go b/pkg/collector/spec.go index e094e4d65..2441b9534 100644 --- a/pkg/collector/spec.go +++ b/pkg/collector/spec.go @@ -24,6 +24,8 @@ import ( "errors" "fmt" "net/url" + "os" + "path/filepath" "regexp" "slices" "strings" @@ -1042,7 +1044,7 @@ func checkServiceRequirementErrors(allServiceRequirements []*ServiceRequirements } // convert service requirements to a cloud bill of materials -func ServiceRequirementsToSpec(projectName string, environmentVariables map[string]string, allServiceRequirements []*ServiceRequirements, allBatchRequirements []*BatchRequirements) (*deploymentspb.Spec, error) { +func ServiceRequirementsToSpec(projectName string, environmentVariables map[string]string, allServiceRequirements []*ServiceRequirements, allBatchRequirements []*BatchRequirements, websiteRequirements []*deploymentspb.Website) (*deploymentspb.Spec, error) { if err := checkServiceRequirementErrors(allServiceRequirements, allBatchRequirements); err != nil { return nil, err } @@ -1180,6 +1182,24 @@ func ServiceRequirementsToSpec(projectName string, environmentVariables map[stri }) } + for _, website := range websiteRequirements { + cleanedPath := strings.TrimRight(website.OutputDirectory, string(os.PathSeparator)) + // Get the parent directory + parentDir := filepath.Dir(cleanedPath) + // Extract the directory name from the parent path + _, name := filepath.Split(parentDir) + + newSpec.Resources = append(newSpec.Resources, &deploymentspb.Resource{ + Id: &resourcespb.ResourceIdentifier{ + Name: name, + Type: resourcespb.ResourceType_Website, + }, + Config: &deploymentspb.Resource_Website{ + Website: website, + }, + }) + } + return newSpec, projectErrors.Error() } diff --git a/pkg/project/config.go b/pkg/project/config.go index 6e815f01c..ad50ea26b 100644 --- a/pkg/project/config.go +++ b/pkg/project/config.go @@ -84,12 +84,33 @@ type BatchConfiguration struct { BaseServiceConfiguration `yaml:",inline"` } +type Build struct { + Command string `yaml:"command"` + Output string `yaml:"output"` +} + +type Dev struct { + Command string `yaml:"command"` + Url string `yaml:"url,omitempty"` +} + +type WebsiteConfiguration struct { + BaseServiceConfiguration `yaml:",inline"` + + Build Build `yaml:"build"` + Dev Dev `yaml:"dev"` + Path string `yaml:"path"` + IndexPage string `yaml:"index,omitempty"` + ErrorPage string `yaml:"error,omitempty"` +} + type ProjectConfiguration struct { Name string `yaml:"name"` Directory string `yaml:"-"` Services []ServiceConfiguration `yaml:"services"` Ports map[string]int `yaml:"ports,omitempty"` Batches []BatchConfiguration `yaml:"batch-services"` + Websites []WebsiteConfiguration `yaml:"websites"` Runtimes map[string]RuntimeConfiguration `yaml:"runtimes,omitempty"` Preview []preview.Feature `yaml:"preview,omitempty"` } diff --git a/pkg/project/project.go b/pkg/project/project.go index 9177acf91..7438a41b3 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -39,11 +39,13 @@ import ( goruntime "runtime" "github.com/nitrictech/cli/pkg/cloud" + "github.com/nitrictech/cli/pkg/cloud/websites" "github.com/nitrictech/cli/pkg/collector" "github.com/nitrictech/cli/pkg/preview" "github.com/nitrictech/cli/pkg/project/localconfig" "github.com/nitrictech/cli/pkg/project/runtime" "github.com/nitrictech/nitric/core/pkg/logger" + deploymentpb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1" ) type Project struct { @@ -54,6 +56,7 @@ type Project struct { services []Service batches []Batch + websites []Website } func (p *Project) GetServices() []Service { @@ -64,6 +67,10 @@ func (p *Project) GetBatchServices() []Batch { return p.batches } +func (p *Project) GetWebsites() []Website { + return p.websites +} + // TODO: Reduce duplicate code // BuildBatches - Builds all the batches in the project func (p *Project) BuildBatches(fs afero.Fs, useBuilder bool) (chan ServiceBuildUpdate, error) { @@ -176,6 +183,61 @@ func (p *Project) BuildServices(fs afero.Fs, useBuilder bool) (chan ServiceBuild return updatesChan, nil } +// BuildWebsites - Builds all the websites in the project via build command +func (p *Project) BuildWebsites(env map[string]string) (chan ServiceBuildUpdate, error) { + updatesChan := make(chan ServiceBuildUpdate) + + maxConcurrentBuilds := make(chan struct{}, min(goruntime.NumCPU(), goruntime.GOMAXPROCS(0))) + + waitGroup := sync.WaitGroup{} + + for _, website := range p.websites { + waitGroup.Add(1) + // Create writer + serviceBuildUpdateWriter := NewBuildUpdateWriter(website.Name, updatesChan) + + go func(site Website, writer io.Writer) { + // Acquire a token by filling the maxConcurrentBuilds channel + // this will block once the buffer is full + maxConcurrentBuilds <- struct{}{} + + // Start goroutine + if err := site.Build(updatesChan, env); err != nil { + updatesChan <- ServiceBuildUpdate{ + ServiceName: site.Name, + Err: err, + Message: err.Error(), + Status: ServiceBuildStatus_Error, + } + + } else { + updatesChan <- ServiceBuildUpdate{ + ServiceName: site.Name, + Message: "Build Complete", + Status: ServiceBuildStatus_Complete, + } + } + + // release our lock + <-maxConcurrentBuilds + + waitGroup.Done() + }(website, serviceBuildUpdateWriter) + } + + go func() { + waitGroup.Wait() + // Drain the semaphore to make sure all goroutines have finished + for i := 0; i < cap(maxConcurrentBuilds); i++ { + maxConcurrentBuilds <- struct{}{} + } + + close(updatesChan) + }() + + return updatesChan, nil +} + func (p *Project) collectServiceRequirements(service Service) (*collector.ServiceRequirements, error) { serviceRequirements := collector.NewServiceRequirements(service.Name, service.GetFilePath(), service.Type) @@ -390,6 +452,27 @@ func (p *Project) CollectBatchRequirements() ([]*collector.BatchRequirements, er return allBatchRequirements, nil } +func (p *Project) CollectWebsiteRequirements() ([]*deploymentpb.Website, error) { + allWebsiteRequirements := []*deploymentpb.Website{} + + for _, site := range p.websites { + outputDir, err := site.GetAbsoluteOutputPath() + if err != nil { + return nil, fmt.Errorf("unable to get absolute output path for website %s: %w", site.basedir, err) + } + + allWebsiteRequirements = append(allWebsiteRequirements, &deploymentpb.Website{ + BasePath: site.path, + OutputDirectory: outputDir, + IndexDocument: site.indexPage, + ErrorDocument: site.errorPage, + }) + + } + + return allWebsiteRequirements, nil +} + // DefaultMigrationImage - Returns the default migration image name for the project // Also returns ok if image is required or not func (p *Project) DefaultMigrationImage(fs afero.Fs) (string, bool) { @@ -526,6 +609,66 @@ func (p *Project) RunServices(localCloud *cloud.LocalCloud, stop <-chan bool, up return group.Wait() } +// RunWebsites - Runs all the websites as http servers +// TODO this has duplicate code with CollectWebsiteRequirements +func (p *Project) RunWebsites(localCloud *cloud.LocalCloud) error { + sites := []websites.Website{} + + // register websites with the local cloud + for _, site := range p.websites { + outputDir, err := site.GetAbsoluteOutputPath() + if err != nil { + return fmt.Errorf("unable to get absolute output path for website %s: %w", site.basedir, err) + } + + sites = append(sites, websites.Website{ + Name: site.Name, + WebsitePb: websites.WebsitePb{ + BasePath: site.outputPath, + OutputDirectory: outputDir, + IndexDocument: site.indexPage, + ErrorDocument: site.errorPage, + }, + }) + } + + return localCloud.Websites.Start(sites) +} + +// RunWebsitesWithCommand - Runs all the websites using a startup command +// use the stop channel to stop all running websites +func (p *Project) RunWebsitesWithCommand(localCloud *cloud.LocalCloud, stop <-chan bool, updates chan<- ServiceRunUpdate, env map[string]string) error { + stopChannels := lo.FanOut[bool](len(p.websites), 1, stop) + + group, _ := errgroup.WithContext(context.TODO()) + + for i, site := range p.websites { + idx := i + s := site + + // start the service with the given file reference from its projects CWD + group.Go(func() error { + envVariables := map[string]string{ + "PYTHONUNBUFFERED": "TRUE", // ensure all print statements print immediately for python + "NITRIC_ENVIRONMENT": "run", + } + + for key, value := range env { + envVariables[key] = value + } + + err := s.Run(stopChannels[idx], updates, envVariables) + if err != nil { + return fmt.Errorf("%s: %w", s.Name, err) + } + + return nil + }) + } + + return group.Wait() +} + func (pc *ProjectConfiguration) pathToNormalizedServiceName(servicePath string) string { // Add the project name as a prefix to group service images servicePath = fmt.Sprintf("%s_%s", pc.Name, servicePath) @@ -545,6 +688,7 @@ func (pc *ProjectConfiguration) pathToNormalizedServiceName(servicePath string) func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig *localconfig.LocalConfiguration, fs afero.Fs) (*Project, error) { services := []Service{} batches := []Batch{} + websites := []Website{} matches := map[string]string{} @@ -654,6 +798,53 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig * } } + for _, websiteSpec := range projectConfig.Websites { + if websiteSpec.Build.Output == "" { + return nil, fmt.Errorf("no build output provided for website %s", websiteSpec.GetBasedir()) + } + + // apply defaults + if websiteSpec.Path == "" { + websiteSpec.Path = "/" + } + + if websiteSpec.IndexPage == "" { + websiteSpec.IndexPage = "index.html" + } + + if websiteSpec.ErrorPage == "" { + websiteSpec.ErrorPage = "index.html" + } + + projectRelativeWebsiteFolder := filepath.Join(projectConfig.Directory, websiteSpec.GetBasedir()) + + websiteName := fmt.Sprintf("websites_%s", strings.ToLower(projectRelativeWebsiteFolder)) + + websites = append(websites, Website{ + Name: websiteName, + basedir: websiteSpec.GetBasedir(), + path: websiteSpec.Path, + outputPath: websiteSpec.Build.Output, + buildCmd: websiteSpec.Build.Command, + devCmd: websiteSpec.Dev.Command, + indexPage: websiteSpec.IndexPage, + errorPage: websiteSpec.ErrorPage, + }) + } + + // check for duplicate paths in websites and error + siteDuplicates := lo.FindDuplicatesBy(websites, func(website Website) string { + return website.path + }) + + if len(siteDuplicates) > 0 { + duplicatePaths := lo.Map(siteDuplicates, func(website Website, i int) string { + return website.path + }) + + return nil, fmt.Errorf("duplicate website paths found: %s", strings.Join(duplicatePaths, ", ")) + } + // create an empty local configuration if none is provided if localConfig == nil { localConfig = &localconfig.LocalConfiguration{} @@ -666,6 +857,7 @@ func fromProjectConfiguration(projectConfig *ProjectConfiguration, localConfig * LocalConfig: *localConfig, services: services, batches: batches, + websites: websites, } if len(project.batches) > 0 && !slices.Contains(project.Preview, preview.Feature_BatchServices) { diff --git a/pkg/project/website.go b/pkg/project/website.go new file mode 100644 index 000000000..281d7dc93 --- /dev/null +++ b/pkg/project/website.go @@ -0,0 +1,209 @@ +// Copyright Nitric Pty Ltd. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package project + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" +) + +type Website struct { + Name string + + // the base directory for the website source files + basedir string + + // the path for the website subroutes, / is the root + path string + + // the build command to build the website + buildCmd string + + // the dev command to run the website + devCmd string + + // the path to the website source files + outputPath string + + // index page for the website + indexPage string + + // error page for the website + errorPage string +} + +func (s *Website) GetOutputPath() string { + return filepath.Join(s.basedir, s.outputPath) +} + +func (s *Website) GetAbsoluteOutputPath() (string, error) { + return filepath.Abs(s.GetOutputPath()) +} + +// Run - runs the website using the provided dev command +func (s *Website) Run(stop <-chan bool, updates chan<- ServiceRunUpdate, env map[string]string) error { + if s.devCmd == "" { + return fmt.Errorf("no dev command provided for website %s", s.basedir) + } + + commandParts := strings.Split(s.devCmd, " ") + cmd := exec.Command( + commandParts[0], + commandParts[1:]..., + ) + + cmd.Env = append([]string{}, os.Environ()...) + cmd.Dir = s.basedir + + for k, v := range env { + cmd.Env = append(cmd.Env, k+"="+v) + } + + cmd.Stdout = &ServiceRunUpdateWriter{ + updates: updates, + serviceName: s.Name, + label: s.Name, + status: ServiceRunStatus_Running, + } + + cmd.Stderr = &ServiceRunUpdateWriter{ + updates: updates, + serviceName: s.Name, + label: s.Name, + status: ServiceRunStatus_Error, + } + + errChan := make(chan error) + + go func() { + err := cmd.Start() + if err != nil { + errChan <- fmt.Errorf("error starting website %s: %w", s.Name, err) + } else { + updates <- ServiceRunUpdate{ + ServiceName: s.Name, + Label: "nitric", + Status: ServiceRunStatus_Running, + Message: fmt.Sprintf("started website %s", s.Name), + } + } + + err = cmd.Wait() + if err != nil { + // provide runtime errors as a run update rather than as a fatal error + updates <- ServiceRunUpdate{ + ServiceName: s.Name, + Label: "nitric", + Status: ServiceRunStatus_Error, + Err: err, + } + } + + errChan <- nil + }() + + go func(cmd *exec.Cmd) { + <-stop + + err := cmd.Process.Signal(syscall.SIGTERM) + if err != nil { + _ = cmd.Process.Kill() + } + }(cmd) + + err := <-errChan + updates <- ServiceRunUpdate{ + ServiceName: s.Name, + Status: ServiceRunStatus_Error, + Err: err, + } + + return err +} + +// Build - builds the website using the provided command +func (s *Website) Build(updates chan ServiceBuildUpdate, env map[string]string) error { + if s.buildCmd == "" { + return fmt.Errorf("no build command provided for website %s", s.basedir) + } + + commandParts := strings.Split(s.buildCmd, " ") + cmd := exec.Command( + commandParts[0], + commandParts[1:]..., + ) + + cmd.Env = append([]string{}, os.Environ()...) + cmd.Dir = s.basedir + + for k, v := range env { + cmd.Env = append(cmd.Env, k+"="+v) + } + + cmd.Stdout = &serviceBuildUpdateWriter{ + buildUpdateChan: updates, + serviceName: s.Name, + } + + cmd.Stderr = &serviceBuildUpdateWriter{ + buildUpdateChan: updates, + serviceName: s.Name, + } + + errChan := make(chan error) + + go func() { + err := cmd.Start() + if err != nil { + errChan <- fmt.Errorf("error building website %s: %w", s.Name, err) + } else { + updates <- ServiceBuildUpdate{ + ServiceName: s.Name, + Status: ServiceBuildStatus_InProgress, + Message: fmt.Sprintf("building website %s", s.GetOutputPath()), + } + } + + err = cmd.Wait() + if err != nil { + // provide runtime errors as a run update rather than as a fatal error + updates <- ServiceBuildUpdate{ + ServiceName: s.Name, + Status: ServiceBuildStatus_Error, + Err: err, + } + } + + errChan <- nil + }() + + err := <-errChan + + if err != nil { + updates <- ServiceBuildUpdate{ + ServiceName: s.Name, + Status: ServiceBuildStatus_Error, + Err: err, + } + } + + return err +} diff --git a/pkg/view/tui/commands/local/run.go b/pkg/view/tui/commands/local/run.go index 5fc313001..7aac736a1 100644 --- a/pkg/view/tui/commands/local/run.go +++ b/pkg/view/tui/commands/local/run.go @@ -20,6 +20,7 @@ import ( "fmt" "slices" "sort" + "strings" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -32,6 +33,7 @@ import ( "github.com/nitrictech/cli/pkg/cloud/schedules" "github.com/nitrictech/cli/pkg/cloud/sql" "github.com/nitrictech/cli/pkg/cloud/topics" + "github.com/nitrictech/cli/pkg/cloud/websites" "github.com/nitrictech/cli/pkg/cloud/websockets" "github.com/nitrictech/cli/pkg/validation" "github.com/nitrictech/cli/pkg/view/tui" @@ -74,6 +76,11 @@ type DatabaseSummary struct { status string } +type WebsiteSummary struct { + name string + url string +} + type TuiModel struct { localCloud *cloud.LocalCloud apis []ApiSummary @@ -82,6 +89,7 @@ type TuiModel struct { topics []TopicSummary schedules []ScheduleSummary databases []DatabaseSummary + websites []WebsiteSummary resources *resources.LocalResourcesState @@ -103,6 +111,7 @@ func (t *TuiModel) Init() tea.Cmd { reactive.ListenFor(t.reactiveSub, t.localCloud.Schedules.SubscribeToState) reactive.ListenFor(t.reactiveSub, t.localCloud.Topics.SubscribeToState) + reactive.ListenFor(t.reactiveSub, t.localCloud.Websites.SubscribeToState) return t.reactiveSub.AwaitNextMsg() } @@ -222,6 +231,17 @@ func (t *TuiModel) ReactiveUpdate(msg tea.Msg) (tea.Model, tea.Cmd) { } t.schedules = newSchedulesSummary + case websites.State: + newWebsitesSummary := []WebsiteSummary{} + + for websiteName, url := range state { + newWebsitesSummary = append(newWebsitesSummary, WebsiteSummary{ + name: strings.TrimPrefix(websiteName, "websites_"), + url: url, + }) + } + + t.websites = newWebsitesSummary } return t, t.reactiveSub.AwaitNextMsg() @@ -289,6 +309,11 @@ func (t *TuiModel) View() string { v.Addln(database.status).WithStyle(textHighlight) } + for _, site := range t.websites { + v.Addf("site:%s - ", site.name) + v.Addln(site.url).WithStyle(textHighlight) + } + if t.resources != nil { if len(t.resources.ServiceErrors) > 0 { v.Break()