Dodo APP: infrastructure to deploy app by pusing to Git repo
Change-Id: I4034c6893255581b014ddb207c844261cb34202b
diff --git a/apps/app-runner/.gitignore b/apps/app-runner/.gitignore
new file mode 100644
index 0000000..f7f9db4
--- /dev/null
+++ b/apps/app-runner/.gitignore
@@ -0,0 +1 @@
+app-runner_*
diff --git a/apps/app-runner/Dockerfile b/apps/app-runner/Dockerfile
new file mode 100644
index 0000000..964cd31
--- /dev/null
+++ b/apps/app-runner/Dockerfile
@@ -0,0 +1,5 @@
+FROM golang:1.22.0-bookworm
+
+ARG TARGETARCH
+
+COPY app-runner_${TARGETARCH} /usr/bin/app-runner
diff --git a/apps/app-runner/Makefile b/apps/app-runner/Makefile
new file mode 100644
index 0000000..4c2424e
--- /dev/null
+++ b/apps/app-runner/Makefile
@@ -0,0 +1,35 @@
+repo_name ?= giolekva
+podman ?= docker
+ifeq ($(podman), podman)
+manifest_dest=docker://docker.io/$(repo_name)/app-runner:golang-1.22.0
+endif
+
+clean:
+ rm -f app-runner
+
+build_arm64: export CGO_ENABLED=0
+build_arm64: export GO111MODULE=on
+build_arm64: export GOOS=linux
+build_arm64: export GOARCH=arm64
+build_arm64:
+ /usr/local/go/bin/go build -o app-runner_arm64 *.go
+
+build_amd64: export CGO_ENABLED=0
+build_amd64: export GO111MODULE=on
+build_amd64: export GOOS=linux
+build_amd64: export GOARCH=amd64
+build_amd64:
+ /usr/local/go/bin/go build -o app-runner_amd64 *.go
+
+push_arm64: clean build_arm64
+ $(podman) build --platform linux/arm64 --tag=$(repo_name)/app-runner:golang-1.22.0-arm64 .
+ $(podman) push $(repo_name)/app-runner:golang-1.22.0-arm64
+
+push_amd64: clean build_amd64
+ $(podman) build --platform linux/amd64 --tag=$(repo_name)/app-runner:golang-1.22.0-amd64 .
+ $(podman) push $(repo_name)/app-runner:golang-1.22.0-amd64
+
+push: push_arm64 push_amd64
+ $(podman) manifest create $(repo_name)/app-runner:golang-1.22.0 $(repo_name)/app-runner:golang-1.22.0-arm64 $(repo_name)/app-runner:golang-1.22.0-amd64
+ $(podman) manifest push $(repo_name)/app-runner:golang-1.22.0 $(manifest_dest)
+ $(podman) manifest rm $(repo_name)/app-runner:golang-1.22.0
diff --git a/apps/app-runner/go.mod b/apps/app-runner/go.mod
new file mode 100644
index 0000000..5ee731f
--- /dev/null
+++ b/apps/app-runner/go.mod
@@ -0,0 +1,31 @@
+module github.com/giolekva/pcloud/apps/app-runner
+
+go 1.18
+
+require (
+ github.com/go-git/go-billy/v5 v5.5.0
+ github.com/go-git/go-git/v5 v5.12.0
+ golang.org/x/crypto v0.23.0
+)
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/ProtonMail/go-crypto v1.0.0 // indirect
+ github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/pjbgf/sha1cd v0.3.0 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+ github.com/skeema/knownhosts v1.2.2 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ golang.org/x/mod v0.12.0 // indirect
+ golang.org/x/net v0.22.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/tools v0.13.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+)
diff --git a/apps/app-runner/go.sum b/apps/app-runner/go.sum
new file mode 100644
index 0000000..602310e
--- /dev/null
+++ b/apps/app-runner/go.sum
@@ -0,0 +1,130 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
+github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
+github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
+github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
+github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
+github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/apps/app-runner/main.go b/apps/app-runner/main.go
new file mode 100644
index 0000000..fb17415
--- /dev/null
+++ b/apps/app-runner/main.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "net"
+ "os"
+
+ "golang.org/x/crypto/ssh"
+
+ "github.com/go-git/go-billy/v5/osfs"
+ "github.com/go-git/go-git/v5"
+ gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
+ "github.com/go-git/go-git/v5/storage/memory"
+)
+
+var port = flag.Int("port", 3000, "Port to listen on")
+var repoAddr = flag.String("repo-addr", "", "Git repository address")
+var sshKey = flag.String("ssh-key", "", "Private SSH key to access Git repository")
+var appDir = flag.String("app-dir", "", "Path to store application repository locally")
+var runCfg = flag.String("run-cfg", "", "Run configuration")
+var manager = flag.String("manager", "", "Address of the manager")
+
+type Command struct {
+ Bin string `json:"bin"`
+ Args []string `json:"args"`
+}
+
+func CloneRepository(addr string, signer ssh.Signer, path string) error {
+ _, err := git.Clone(memory.NewStorage(), osfs.New(path, osfs.WithBoundOS()), &git.CloneOptions{
+ URL: addr,
+ Auth: &gitssh.PublicKeys{
+ User: "git",
+ Signer: signer,
+ HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
+ HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+ // TODO(giolekva): verify server public key
+ fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
+ return nil
+ },
+ },
+ },
+ RemoteName: "origin",
+ ReferenceName: "refs/heads/master",
+ Depth: 1,
+ InsecureSkipTLS: true,
+ })
+ return err
+}
+
+func main() {
+ flag.Parse()
+ self, ok := os.LookupEnv("SELF_IP")
+ if !ok {
+ panic("no SELF_IP")
+ }
+ key, err := os.ReadFile(*sshKey)
+ if err != nil {
+ panic(err)
+ }
+ signer, err := ssh.ParsePrivateKey(key)
+ if err != nil {
+ panic(err)
+ }
+ if err := CloneRepository(*repoAddr, signer, *appDir); err != nil {
+ panic(err)
+ }
+ r, err := os.Open(*runCfg)
+ if err != nil {
+ panic(err)
+ }
+ defer r.Close()
+ var cmds []Command
+ if err := json.NewDecoder(r).Decode(&cmds); err != nil {
+ panic(err)
+ }
+ s := NewServer(*port, *repoAddr, signer, *appDir, cmds, self, *manager)
+ s.Start()
+}
diff --git a/apps/app-runner/server.go b/apps/app-runner/server.go
new file mode 100644
index 0000000..665f493
--- /dev/null
+++ b/apps/app-runner/server.go
@@ -0,0 +1,135 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "os/exec"
+ "sync"
+ "time"
+
+ "golang.org/x/crypto/ssh"
+)
+
+type Server struct {
+ l sync.Locker
+ port int
+ ready bool
+ cmd *exec.Cmd
+ repoAddr string
+ signer ssh.Signer
+ appDir string
+ runCommands []Command
+ self string
+ manager string
+}
+
+func NewServer(port int, repoAddr string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
+ return &Server{
+ l: &sync.Mutex{},
+ port: port,
+ ready: false,
+ repoAddr: repoAddr,
+ signer: signer,
+ appDir: appDir,
+ runCommands: runCommands,
+ self: self,
+ manager: manager,
+ }
+}
+
+func (s *Server) Start() error {
+ http.HandleFunc("/update", s.handleUpdate)
+ http.HandleFunc("/ready", s.handleReady)
+ if err := s.run(); err != nil {
+ return err
+ }
+ go s.pingManager()
+ return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
+}
+
+func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
+ s.l.Lock()
+ defer s.l.Unlock()
+ if s.ready {
+ fmt.Fprintln(w, "ok")
+ } else {
+ http.Error(w, "not ready", http.StatusInternalServerError)
+ }
+}
+
+func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
+ fmt.Println("update")
+ s.l.Lock()
+ s.ready = false
+ s.l.Unlock()
+ if s.cmd != nil {
+ err := s.cmd.Process.Kill()
+ s.cmd.Wait()
+ s.cmd = nil
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+ if err := os.RemoveAll(s.appDir); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := s.run(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ s.l.Lock()
+ s.ready = true
+ s.l.Unlock()
+}
+
+func (s *Server) run() error {
+ if err := CloneRepository(s.repoAddr, s.signer, s.appDir); err != nil {
+ return err
+ }
+ for i, c := range s.runCommands {
+ args := []string{c.Bin}
+ args = append(args, c.Args...)
+ cmd := &exec.Cmd{
+ Dir: *appDir,
+ Path: c.Bin,
+ Args: args,
+ Stdout: os.Stdout,
+ Stderr: os.Stderr,
+ }
+ fmt.Printf("Running: %s\n", c.Bin)
+ if i < len(s.runCommands)-1 {
+ if err := cmd.Run(); err != nil {
+ return err
+ }
+ } else {
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ s.cmd = cmd
+ }
+ }
+ return nil
+}
+
+type pingReq struct {
+ Address string `json:"address"`
+}
+
+func (s *Server) pingManager() {
+ defer func() {
+ go func() {
+ time.Sleep(5 * time.Second)
+ s.pingManager()
+ }()
+ }()
+ buf, err := json.Marshal(pingReq{s.self})
+ if err != nil {
+ return
+ }
+ http.Post(s.manager, "application/json", bytes.NewReader(buf))
+}
diff --git a/apps/app-runner/test-cfg.json b/apps/app-runner/test-cfg.json
new file mode 100644
index 0000000..235e6ee
--- /dev/null
+++ b/apps/app-runner/test-cfg.json
@@ -0,0 +1,9 @@
+[
+ {
+ "bin": "/usr/local/go/bin/go",
+ "args": ["mod", "tidy"]
+}, {
+ "bin": "/usr/local/go/bin/go",
+ "args": ["run", "main.go", "--port=8081"]
+}
+]
diff --git a/charts/app-runner/.helmignore b/charts/app-runner/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/app-runner/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/app-runner/Chart.yaml b/charts/app-runner/Chart.yaml
new file mode 100644
index 0000000..b1bad90
--- /dev/null
+++ b/charts/app-runner/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: app-runner
+description: A Helm chart for PCloud App Runner
+type: application
+version: 0.0.1
+appVersion: "0.0.1"
diff --git a/charts/app-runner/templates/install.yaml b/charts/app-runner/templates/install.yaml
new file mode 100644
index 0000000..5bdf72e
--- /dev/null
+++ b/charts/app-runner/templates/install.yaml
@@ -0,0 +1,101 @@
+{{ $runCfg := .Values.runCfg | b64dec }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: app-ssh-key
+type: Opaque
+data:
+ private: {{ .Values.sshPrivateKey }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: app-run-cfg
+data:
+ run: |
+{{ indent 4 $runCfg }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: app-app
+ namespace: {{ .Release.Namespace }}
+spec:
+ type: ClusterIP
+ selector:
+ app: app-app
+ ports:
+ - name: app
+ port: 80
+ targetPort: app
+ protocol: TCP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: app-api
+ namespace: {{ .Release.Namespace }}
+spec:
+ type: ClusterIP
+ selector:
+ app: app-app
+ ports:
+ - name: api
+ port: 3000
+ targetPort: api
+ protocol: TCP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: app-app
+ namespace: {{ .Release.Namespace }}
+spec:
+ selector:
+ matchLabels:
+ app: app-app
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: app-app
+ spec:
+ volumes:
+ - name: ssh-key
+ secret:
+ secretName: app-ssh-key
+ - name: run-cfg
+ configMap:
+ name: app-run-cfg
+ containers:
+ - name: app
+ image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - name: api
+ containerPort: 3000
+ protocol: TCP
+ - name: app
+ containerPort: {{ .Values.appPort }}
+ protocol: TCP
+ env:
+ - name: SELF_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ command:
+ - app-runner
+ - --port=3000
+ - --app-dir=/dodo-app
+ - --repo-addr={{ .Values.repoAddr }}
+ - --ssh-key=/pcloud/ssh-key/private
+ - --run-cfg=/pcloud/config/run
+ - --manager={{ .Values.manager }}
+ volumeMounts:
+ - name: ssh-key
+ readOnly: true
+ mountPath: /pcloud/ssh-key
+ - name: run-cfg
+ readOnly: true
+ mountPath: /pcloud/config
diff --git a/charts/app-runner/values.yaml b/charts/app-runner/values.yaml
new file mode 100644
index 0000000..9ec9b55
--- /dev/null
+++ b/charts/app-runner/values.yaml
@@ -0,0 +1,10 @@
+image:
+ repository: giolekva/app-runner
+ tag: latest
+ pullPolicy: Always
+repoAddr: 192.168.0.11
+sshPrivateKey: key
+runCfg: ""
+appDir: /dodo-app
+appPort: 8080
+manager: ""
diff --git a/charts/dodo-app/.helmignore b/charts/dodo-app/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/dodo-app/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/dodo-app/Chart.yaml b/charts/dodo-app/Chart.yaml
new file mode 100644
index 0000000..b91b7b9
--- /dev/null
+++ b/charts/dodo-app/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: dodo-app
+description: A Helm chart for updaring Dodo apps
+type: application
+version: 0.0.1
+appVersion: "0.0.1"
diff --git a/charts/dodo-app/templates/install.yaml b/charts/dodo-app/templates/install.yaml
new file mode 100644
index 0000000..f19ea1e
--- /dev/null
+++ b/charts/dodo-app/templates/install.yaml
@@ -0,0 +1,77 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: ssh-key
+type: Opaque
+data:
+ private: {{ .Values.sshPrivateKey }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: dodo-app
+ namespace: {{ .Release.Namespace }}
+spec:
+ type: ClusterIP
+ selector:
+ app: dodo-app
+ ports:
+ - name: http
+ port: 80
+ targetPort: http
+ protocol: TCP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: dodo-app
+ namespace: {{ .Release.Namespace }}
+spec:
+ selector:
+ matchLabels:
+ app: dodo-app
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: dodo-app
+ spec:
+ volumes:
+ - name: ssh-key
+ secret:
+ secretName: ssh-key
+ - name: env-config
+ secret:
+ secretName: env-config
+ containers:
+ - name: dodo-app
+ image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ command:
+ - pcloud-installer
+ - dodo-app
+ - --repo-addr={{ .Values.repoAddr }}
+ - --ssh-key=/pcloud/ssh-key/private
+ - --port=8080
+ - --self={{ .Values.self }}
+ - --namespace={{ .Values.namespace }} # TODO(gio): maybe use .Release.Namespace ?
+ - --env-config=/pcloud/env-config/config.json
+ volumeMounts:
+ - name: ssh-key
+ readOnly: true
+ mountPath: /pcloud/ssh-key
+ - name: env-config
+ readOnly: true
+ mountPath: /pcloud/env-config
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: env-config
+type: Opaque
+data:
+ config.json: {{ .Values.envConfig }}
diff --git a/charts/dodo-app/values.yaml b/charts/dodo-app/values.yaml
new file mode 100644
index 0000000..dcdc380
--- /dev/null
+++ b/charts/dodo-app/values.yaml
@@ -0,0 +1,9 @@
+image:
+ repository: giolekva/pcloud-installer
+ tag: latest
+ pullPolicy: Always
+repoAddr: 192.168.0.11
+sshPrivateKey: key
+self: ""
+namespace: ""
+envConfig: ""
diff --git a/charts/soft-serve/templates/stateful-set.yaml b/charts/soft-serve/templates/stateful-set.yaml
index 0d84eca..0ed35ea 100644
--- a/charts/soft-serve/templates/stateful-set.yaml
+++ b/charts/soft-serve/templates/stateful-set.yaml
@@ -33,7 +33,8 @@
- name: SOFT_SERVE_SSH_PUBLIC_URL
value: "ssh://{{ .Values.ingress.domain }}:{{ .Values.sshPublicPort }}"
- name: SOFT_SERVE_INITIAL_ADMIN_KEYS
- value: "{{ .Values.adminKey }}"
+ value: |-
+{{ indent 12 .Values.adminKey }}
{{ if and .Values.privateKey .Values.publicKey }}
- name: SOFT_SERVE_SSH_KEY_PATH
value: /.ssh/key
diff --git a/core/installer/Makefile b/core/installer/Makefile
index 800a8eb..ce9c184 100644
--- a/core/installer/Makefile
+++ b/core/installer/Makefile
@@ -37,6 +37,9 @@
appmanager:
./pcloud --kubeconfig=../../priv/kubeconfig-hetzner appmanager --ssh-key=/Users/lekva/.ssh/id_ed25519 --repo-addr=ssh://localhost:2222/config --port=9090 # --app-repo-addr=http://localhost:8080
+dodo-app:
+ ./pcloud --kubeconfig=../../priv/kubeconfig-hetzner dodo-app --ssh-key=/Users/lekva/.ssh/id_ed25519 --repo-addr=ssh://localhost:2222/test
+
welc:
./pcloud --kubeconfig=../../priv/kubeconfig welcome --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://192.168.0.210/config --port=9090
diff --git a/core/installer/app.go b/core/installer/app.go
index 1626a1a..2a16c1e 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ _ "embed"
"encoding/json"
"fmt"
template "html/template"
@@ -16,8 +17,15 @@
cueyaml "cuelang.org/go/encoding/yaml"
)
+//go:embed pcloud_app.cue
+var DodoAppCue []byte
+
// TODO(gio): import
const cueEnvAppGlobal = `
+import (
+ "net"
+)
+
#Global: {
id: string | *""
pcloudEnvName: string | *""
@@ -31,20 +39,13 @@
network: #EnvNetwork
}
-networks: {
- public: #Network & {
- name: "Public"
- ingressClass: "\(global.pcloudEnvName)-ingress-public"
- certificateIssuer: "\(global.id)-public"
- domain: global.domain
- allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
- }
- private: #Network & {
- name: "Private"
- ingressClass: "\(global.id)-ingress-private"
- domain: global.privateDomain
- allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
- }
+#EnvNetwork: {
+ dns: net.IPv4
+ dnsInClusterIP: net.IPv4
+ ingress: net.IPv4
+ headscale: net.IPv4
+ servicesFrom: net.IPv4
+ servicesTo: net.IPv4
}
// TODO(gio): remove
@@ -164,10 +165,6 @@
`
const cueBaseConfig = `
-import (
- "net"
-)
-
name: string | *""
description: string | *""
readme: string | *""
@@ -187,9 +184,11 @@
#AppType: "infra" | "env"
appType: #AppType | *"env"
-#Auth: {
- enabled: bool | *false // TODO(gio): enabled by default?
- groups: string | *"" // TODO(gio): []string
+#Release: {
+ appInstanceId: string
+ namespace: string
+ repoAddr: string
+ appDir: string
}
#Network: {
@@ -200,6 +199,11 @@
allocatePortAddr: string
}
+#Auth: {
+ enabled: bool | *false // TODO(gio): enabled by default?
+ groups: string | *"" // TODO(gio): []string
+}
+
#Image: {
registry: string | *"docker.io"
repository: string
@@ -222,22 +226,6 @@
namespace: string // TODO(gio): default global.id
}
-#EnvNetwork: {
- dns: net.IPv4
- dnsInClusterIP: net.IPv4
- ingress: net.IPv4
- headscale: net.IPv4
- servicesFrom: net.IPv4
- servicesTo: net.IPv4
-}
-
-#Release: {
- appInstanceId: string
- namespace: string
- repoAddr: string
- appDir: string
-}
-
#PortForward: {
allocator: string
protocol: "TCP" | "UDP" | *"TCP"
@@ -302,6 +290,8 @@
}
}
+resources: {}
+
#HelmRelease: {
_name: string
_chart: #Chart
@@ -349,6 +339,8 @@
help: [...#HelpDocument] | *[]
url: string | *""
+
+networks: {}
`
type rendered struct {
@@ -620,17 +612,34 @@
if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
return rendered{}, err
}
- output := res.LookupPath(cue.ParsePath("output"))
- i, err := output.Fields()
- if err != nil {
- return rendered{}, err
- }
- for i.Next() {
- if contents, err := cueyaml.Encode(i.Value()); err != nil {
+ {
+ output := res.LookupPath(cue.ParsePath("output"))
+ i, err := output.Fields()
+ if err != nil {
return rendered{}, err
- } else {
- name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
- ret.Resources[name] = contents
+ }
+ for i.Next() {
+ if contents, err := cueyaml.Encode(i.Value()); err != nil {
+ return rendered{}, err
+ } else {
+ name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
+ ret.Resources[name] = contents
+ }
+ }
+ }
+ {
+ resources := res.LookupPath(cue.ParsePath("resources"))
+ i, err := resources.Fields()
+ if err != nil {
+ return rendered{}, err
+ }
+ for i.Next() {
+ if contents, err := cueyaml.Encode(i.Value()); err != nil {
+ return rendered{}, err
+ } else {
+ name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
+ ret.Resources[name] = contents
+ }
}
}
helpValue := res.LookupPath(cue.ParsePath("help"))
@@ -664,6 +673,15 @@
return cueEnvApp{app}, nil
}
+func NewDodoApp(appCfg []byte) (EnvApp, error) {
+ return NewCueEnvApp(CueAppData{
+ "app.cue": appCfg,
+ "base.cue": []byte(cueBaseConfig),
+ "pcloud_app.cue": DodoAppCue,
+ "env_app.cue": []byte(cueEnvAppGlobal),
+ })
+}
+
func (a cueEnvApp) Type() AppType {
return AppTypeEnv
}
@@ -675,9 +693,10 @@
return EnvAppRendered{}, nil
}
ret, err := a.cueApp.render(map[string]any{
- "global": env,
- "release": release,
- "input": derived,
+ "global": env,
+ "release": release,
+ "input": derived,
+ "networks": networkMap(networks),
})
if err != nil {
return EnvAppRendered{}, err
@@ -747,3 +766,11 @@
}
return strings.Join(tmp, ",")
}
+
+func networkMap(networks []Network) map[string]Network {
+ ret := make(map[string]Network)
+ for _, n := range networks {
+ ret[strings.ToLower(n.Name)] = n
+ }
+ return ret
+}
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 44e39e6..ae18ff8 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -178,7 +178,7 @@
}
// TODO(gio): rename to CommitApp
-func InstallApp(
+func installApp(
repo soft.RepoIO,
appDir string,
name string,
@@ -242,7 +242,11 @@
}
// TODO(gio): commit instanceId -> appDir mapping as well
-func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
+func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any, opts ...InstallOption) (ReleaseResources, error) {
+ o := &installOptions{}
+ for _, i := range opts {
+ i(o)
+ }
appDir = filepath.Clean(appDir)
if err := m.repoIO.Pull(); err != nil {
return ReleaseResources{}, err
@@ -250,9 +254,15 @@
if err := m.nsCreator.Create(namespace); err != nil {
return ReleaseResources{}, err
}
- env, err := m.Config()
- if err != nil {
- return ReleaseResources{}, err
+ var env EnvConfig
+ if o.Env != nil {
+ env = *o.Env
+ } else {
+ var err error
+ env, err = m.Config()
+ if err != nil {
+ return ReleaseResources{}, err
+ }
}
release := Release{
AppInstanceId: instanceId,
@@ -264,7 +274,12 @@
if err != nil {
return ReleaseResources{}, err
}
- if _, err := InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data); err != nil {
+ dopts := []soft.DoOption{}
+ if o.Branch != "" {
+ dopts = append(dopts, soft.WithForce())
+ dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
+ }
+ if _, err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, dopts...); err != nil {
return ReleaseResources{}, err
}
// TODO(gio): add ingress-nginx to release resources
@@ -278,6 +293,7 @@
type helmRelease struct {
Metadata Resource `json:"metadata"`
+ Kind string `json:"kind"`
Status struct {
Conditions []struct {
Type string `json:"type"`
@@ -293,7 +309,9 @@
if err := yaml.Unmarshal(contents, &h); err != nil {
panic(err) // TODO(gio): handle
}
- ret = append(ret, h.Metadata)
+ if h.Kind == "HelmRelease" {
+ ret = append(ret, h.Metadata)
+ }
}
return ret
}
@@ -322,7 +340,7 @@
if err != nil {
return ReleaseResources{}, err
}
- return InstallApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
+ return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
}
func (m *AppManager) Remove(instanceId string) error {
@@ -368,6 +386,25 @@
nsCreator NamespaceCreator
}
+type installOptions struct {
+ Env *EnvConfig
+ Branch string
+}
+
+type InstallOption func(*installOptions)
+
+func WithConfig(env *EnvConfig) InstallOption {
+ return func(o *installOptions) {
+ o.Env = env
+ }
+}
+
+func WithBranch(branch string) InstallOption {
+ return func(o *installOptions) {
+ o.Branch = branch
+ }
+}
+
func NewInfraAppManager(repoIO soft.RepoIO, nsCreator NamespaceCreator) (*InfraAppManager, error) {
return &InfraAppManager{
repoIO,
@@ -432,7 +469,7 @@
if err != nil {
return ReleaseResources{}, err
}
- return InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
+ return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
}
func (m *InfraAppManager) Update(app InfraApp, instanceId string, values map[string]any, opts ...soft.DoOption) (ReleaseResources, error) {
@@ -459,5 +496,5 @@
if err != nil {
return ReleaseResources{}, err
}
- return InstallApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
+ return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
}
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
index b51f766..f35eb97 100644
--- a/core/installer/app_repository.go
+++ b/core/installer/app_repository.go
@@ -18,6 +18,7 @@
var valuesTmpls embed.FS
var storeEnvAppConfigs = []string{
+ "values-tmpl/dodo-app.cue",
"values-tmpl/url-shortener.cue",
"values-tmpl/matrix.cue",
"values-tmpl/vaultwarden.cue",
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 9b59d1c..a646425 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -1,6 +1,7 @@
package installer
import (
+ _ "embed"
"net"
"testing"
)
@@ -298,3 +299,19 @@
t.Log(string(r))
}
}
+
+//go:embed testapp.cue
+var testAppCue []byte
+
+type appInput struct {
+ RepoAddr string `json:"repoAddr"`
+ SSHKey string `json:"sshKey"`
+ Network Network `json:"network"`
+}
+
+func TestPCloudApp(t *testing.T) {
+ _, err := NewDodoApp(testAppCue)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
new file mode 100644
index 0000000..d9f9a69
--- /dev/null
+++ b/core/installer/cmd/dodo_app.go
@@ -0,0 +1,175 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/giolekva/pcloud/core/installer"
+ "github.com/giolekva/pcloud/core/installer/soft"
+ "github.com/giolekva/pcloud/core/installer/welcome"
+
+ "github.com/spf13/cobra"
+)
+
+var dodoAppFlags struct {
+ port int
+ sshKey string
+ repoAddr string
+ self string
+ namespace string
+ envConfig string
+}
+
+func dodoAppCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "dodo-app",
+ RunE: dodoAppCmdRun,
+ }
+ cmd.Flags().IntVar(
+ &dodoAppFlags.port,
+ "port",
+ 8080,
+ "",
+ )
+ cmd.Flags().StringVar(
+ &dodoAppFlags.repoAddr,
+ "repo-addr",
+ "",
+ "",
+ )
+ cmd.Flags().StringVar(
+ &dodoAppFlags.sshKey,
+ "ssh-key",
+ "",
+ "",
+ )
+ cmd.Flags().StringVar(
+ &dodoAppFlags.self,
+ "self",
+ "",
+ "",
+ )
+ cmd.Flags().StringVar(
+ &dodoAppFlags.namespace,
+ "namespace",
+ "",
+ "",
+ )
+ cmd.Flags().StringVar(
+ &dodoAppFlags.envConfig,
+ "env-config",
+ "",
+ "",
+ )
+ return cmd
+}
+
+func dodoAppCmdRun(cmd *cobra.Command, args []string) error {
+ envConfig, err := os.Open(dodoAppFlags.envConfig)
+ if err != nil {
+ return err
+ }
+ defer envConfig.Close()
+ var env installer.EnvConfig
+ if err := json.NewDecoder(envConfig).Decode(&env); err != nil {
+ return err
+ }
+ sshKey, err := os.ReadFile(dodoAppFlags.sshKey)
+ if err != nil {
+ return err
+ }
+ softClient, err := soft.NewClient(dodoAppFlags.repoAddr, sshKey, log.Default())
+ if err != nil {
+ return err
+ }
+ if err := softClient.AddRepository("app"); err == nil {
+ repo, err := softClient.GetRepo("app")
+ if err != nil {
+ return err
+ }
+ if err := initRepo(repo); err != nil {
+ return err
+ }
+ if err := welcome.UpdateDodoApp(softClient, dodoAppFlags.namespace, string(sshKey), &env); err != nil {
+ return err
+ }
+ if err := softClient.AddWebhook("app", fmt.Sprintf("http://%s/update", dodoAppFlags.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
+ return err
+ }
+ } else if !errors.Is(err, soft.ErrorAlreadyExists) {
+ return err
+ }
+ s := welcome.NewDodoAppServer(dodoAppFlags.port, string(sshKey), softClient, dodoAppFlags.namespace, env)
+ return s.Start()
+}
+
+const goMod = `module dodo.app
+
+go 1.18
+`
+
+const mainGo = `package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, "Hello from Dodo App!")
+}
+
+func main() {
+ flag.Parse()
+ http.HandleFunc("/", handler)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}
+`
+
+const appCue = `app: {
+ type: "golang:1.22.0"
+ run: "main.go"
+ ingress: {
+ network: "Private" // or Public
+ subdomain: "testapp"
+ auth: enabled: false
+ }
+}
+`
+
+func initRepo(repo soft.RepoIO) error {
+ return repo.Do(func(fs soft.RepoFS) (string, error) {
+ {
+ w, err := fs.Writer("go.mod")
+ if err != nil {
+ return "", err
+ }
+ defer w.Close()
+ fmt.Fprint(w, goMod)
+ }
+ {
+ w, err := fs.Writer("main.go")
+ if err != nil {
+ return "", err
+ }
+ defer w.Close()
+ fmt.Fprintf(w, "%s", mainGo)
+ }
+ {
+ w, err := fs.Writer("app.cue")
+ if err != nil {
+ return "", err
+ }
+ defer w.Close()
+ fmt.Fprint(w, appCue)
+ }
+ return "go web app template", nil
+ })
+}
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
index 5b05381..568efae 100644
--- a/core/installer/cmd/main.go
+++ b/core/installer/cmd/main.go
@@ -28,6 +28,7 @@
rootCmd.AddCommand(welcomeCmd())
rootCmd.AddCommand(rewriteCmd())
rootCmd.AddCommand(launcherCmd())
+ rootCmd.AddCommand(dodoAppCmd())
}
func main() {
diff --git a/core/installer/kube.go b/core/installer/kube.go
index a8ed275..c8251ff 100644
--- a/core/installer/kube.go
+++ b/core/installer/kube.go
@@ -31,6 +31,16 @@
Fetch(addr string) (string, error)
}
+type noOpNamespaceCreator struct{}
+
+func (n *noOpNamespaceCreator) Create(name string) error {
+ return nil
+}
+
+func NewNoOpNamespaceCreator() NamespaceCreator {
+ return &noOpNamespaceCreator{}
+}
+
type realNamespaceCreator struct {
clientset *kubernetes.Clientset
}
diff --git a/core/installer/pcloud_app.cue b/core/installer/pcloud_app.cue
new file mode 100644
index 0000000..d453747
--- /dev/null
+++ b/core/installer/pcloud_app.cue
@@ -0,0 +1,102 @@
+import (
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+)
+
+input: {
+ repoAddr: string
+ sshPrivateKey: string
+}
+
+#AppIngress: {
+ network: string
+ subdomain: string
+ auth: #Auth
+}
+
+_goVer1220: "golang:1.22.0"
+_goVer1200: "golang:1.20.0"
+
+#GoAppTmpl: {
+ type: _goVer1220 | _goVer1200
+ run: string
+ ingress: #AppIngress
+
+ runConfiguration: [{
+ bin: "/usr/local/go/bin/go",
+ args: ["mod", "tidy"]
+ }, {
+ bin: "/usr/local/go/bin/go",
+ args: ["build", "-o", ".app", run]
+ }, {
+ bin: ".app",
+ args: []
+ }]
+}
+
+#GoApp1200: #GoAppTmpl & {
+ type: _goVer1200
+}
+
+#GoApp1220: #GoAppTmpl & {
+ type: _goVer1220
+}
+
+#GoApp: #GoApp1200 | #GoApp1220
+
+app: #GoApp
+
+// output
+
+_app: app
+ingress: {
+ app: {
+ network: networks[strings.ToLower(_app.ingress.network)]
+ subdomain: _app.ingress.subdomain
+ auth: _app.ingress.auth
+ service: {
+ name: "app-app"
+ port: name: "app"
+ }
+ }
+}
+
+images: {
+ app: {
+ repository: "giolekva"
+ name: "app-runner"
+ tag: strings.Replace(_app.type, ":", "-", -1)
+ pullPolicy: "Always"
+ }
+}
+
+charts: {
+ app: {
+ chart: "charts/app-runner"
+ sourceRef: {
+ kind: "GitRepository"
+ name: "pcloud"
+ namespace: global.id
+ }
+ }
+}
+
+helm: {
+ app: {
+ chart: charts.app
+ values: {
+ image: {
+ repository: images.app.fullName
+ tag: images.app.tag
+ pullPolicy: images.app.pullPolicy
+ }
+ appPort: 8080
+ appDir: "/dodo-app"
+ repoAddr: input.repoAddr
+ sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+ runCfg: base64.Encode(null, json.Marshal(_app.runConfiguration))
+ manager: "http://dodo-app.\(release.namespace).svc.cluster.local/register-worker"
+ }
+ }
+}
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 269f3d3..08103be 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -19,6 +19,8 @@
"github.com/go-git/go-git/v5/storage/memory"
)
+var ErrorAlreadyExists = errors.New("already exists")
+
type Client interface {
Address() string
Signer() ssh.Signer
@@ -32,6 +34,7 @@
MakeUserAdmin(name string) error
AddReadWriteCollaborator(repo, user string) error
AddReadOnlyCollaborator(repo, user string) error
+ AddWebhook(repo, url string, opts ...string) error
}
type realClient struct {
@@ -131,6 +134,9 @@
func (ss *realClient) AddRepository(name string) error {
log.Printf("Adding repository %s", name)
+ if err := ss.RunCommand("repo", "info", name); err == nil {
+ return ErrorAlreadyExists
+ }
return ss.RunCommand("repo", "create", name)
}
@@ -144,6 +150,14 @@
return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
}
+func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
+ log.Printf("Adding webhook %s %s", repo, url)
+ return ss.RunCommand(append(
+ []string{"repo", "webhook", "create", repo, url},
+ opts...,
+ )...)
+}
+
type Repository struct {
*git.Repository
Addr RepositoryAddress
diff --git a/core/installer/soft/repoio.go b/core/installer/soft/repoio.go
index 6a5097a..b916d24 100644
--- a/core/installer/soft/repoio.go
+++ b/core/installer/soft/repoio.go
@@ -3,10 +3,12 @@
import (
"encoding/json"
"errors"
+ "fmt"
"io"
"io/fs"
"io/ioutil"
"net"
+ "os"
"path/filepath"
"sync"
"time"
@@ -16,6 +18,7 @@
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/util"
"github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/object"
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
"golang.org/x/crypto/ssh"
@@ -33,6 +36,8 @@
type doOptions struct {
NoCommit bool
+ Force bool
+ ToBranch string
}
type DoOption func(*doOptions)
@@ -43,11 +48,42 @@
}
}
+func WithForce() DoOption {
+ return func(o *doOptions) {
+ o.Force = true
+ }
+}
+
+func WithCommitToBranch(branch string) DoOption {
+ return func(o *doOptions) {
+ o.ToBranch = branch
+ }
+}
+
+type pushOptions struct {
+ ToBranch string
+ Force bool
+}
+
+type PushOption func(*pushOptions)
+
+func WithToBranch(branch string) PushOption {
+ return func(o *pushOptions) {
+ o.ToBranch = branch
+ }
+}
+
+func PushWithForce() PushOption {
+ return func(o *pushOptions) {
+ o.Force = true
+ }
+}
+
type RepoIO interface {
RepoFS
FullAddress() string
Pull() error
- CommitAndPush(message string) error
+ CommitAndPush(message string, opts ...PushOption) error
Do(op DoFn, opts ...DoOption) error
}
@@ -120,8 +156,9 @@
return nil
}
err = wt.Pull(&git.PullOptions{
- Auth: auth(r.signer),
- Force: true,
+ Auth: auth(r.signer),
+ Force: true,
+ Progress: os.Stdout,
})
if err == nil {
return nil
@@ -130,10 +167,15 @@
return nil
}
// TODO(gio): check `remote repository is empty`
+ fmt.Println(err)
return nil
}
-func (r *repoIO) CommitAndPush(message string) error {
+func (r *repoIO) CommitAndPush(message string, opts ...PushOption) error {
+ var o pushOptions
+ for _, i := range opts {
+ i(&o)
+ }
wt, err := r.repo.Worktree()
if err != nil {
return err
@@ -149,10 +191,17 @@
}); err != nil {
return err
}
- return r.repo.Push(&git.PushOptions{
+ gopts := &git.PushOptions{
RemoteName: "origin",
Auth: auth(r.signer),
- })
+ }
+ if o.ToBranch != "" {
+ gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/master:refs/heads/%s", o.ToBranch))}
+ }
+ if o.Force {
+ gopts.Force = true
+ }
+ return r.repo.Push(gopts)
}
func (r *repoIO) Do(op DoFn, opts ...DoOption) error {
@@ -169,7 +218,14 @@
return err
} else {
if !o.NoCommit {
- return r.CommitAndPush(msg)
+ popts := []PushOption{}
+ if o.Force {
+ popts = append(popts, PushWithForce())
+ }
+ if o.ToBranch != "" {
+ popts = append(popts, WithToBranch(o.ToBranch))
+ }
+ return r.CommitAndPush(msg, popts...)
}
}
return nil
@@ -248,3 +304,12 @@
}
return ret, nil
}
+
+func ReadFile(repo RepoFS, path string) ([]byte, error) {
+ r, err := repo.Reader(path)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ return io.ReadAll(r)
+}
diff --git a/core/installer/testapp.cue b/core/installer/testapp.cue
new file mode 100644
index 0000000..2573c5c
--- /dev/null
+++ b/core/installer/testapp.cue
@@ -0,0 +1,12 @@
+app: {
+ type: "golang:1.22.0"
+ run: "main.go"
+ ingress: {
+ network: "private"
+ subdomain: "testapp"
+ auth: enabled: false
+ }
+}
+
+// do create app --type=go[1.22.0] [--run-cmd=(*default main.go)]
+// do create ingress --subdomain=testapp [--network=public (*default private)] [--auth] [--auth-groups="admin" (*default empty)] TODO(gio): port
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
new file mode 100644
index 0000000..5acc6db
--- /dev/null
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -0,0 +1,161 @@
+import (
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+)
+
+input: {
+ network: #Network @name(Network)
+ subdomain: string @name(Subdomain)
+ sshPort: int @name(SSH Port)
+ adminKey: string @name(Admin SSH Public Key)
+
+ // TODO(gio): auto generate
+ ssKeys: #SSHKey
+ fluxKeys: #SSHKey
+ dAppKeys: #SSHKey
+}
+
+name: "Dodo App"
+namespace: "dodo-app"
+readme: "Deploy app by pushing to Git repository"
+description: "Deploy app by pushing to Git repository"
+icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 48 48'><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M2.837 27.257c3.363 2.45 11.566 3.523 12.546 1.4s.424-10.94.424-10.94s-1.763 1.192-2.302.147s.44-2.433 2.319-2.858c-1.96.05-2.221-.571-2.205-.93s.67-1.878 3.527-1.241c-1.6-.751-1.943-2.956 2.352-1.568c-1.421-.735-.36-2.825 1.649-.62c-.261-1.323 1.584-1.46 2.694.907M10.648 34.633a19 19 0 0 0-4.246.719'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M15.144 43.402c3.625-2.482 7.685-6.32 7.293-13.406s-1.6-6.368-.523-7.577s6.924-.99 10.712 3.353c.032-2.874-2.504-5.508-2.504-5.508a33 33 0 0 1 5.53.163c2.852.49 2.394 2.514 3.58 2.035s.971-3.472-.39-5.377c-1.666-2.33-3.223-2.83-6.358-2.188s-4.474.458-5.54-.587s-2.026-3.538-4.605-2.515c-2.935 1.164-4.398 2.438-3.767 5.04s2.34 4.558 2.972 6.844'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M22.001 16.552c-.925-.043-1.894.055-1.709 1.328'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M20.662 16.763c1.72 2.695 3.405 3.643 9.46 3.501'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M32.14 14.966c-1.223.879-2.18 3.781-2.496 5.307M23.1 14.908c.48 1.209 1.23.728 1.315.283a1.552 1.552 0 0 0-1.543-1.883m-.408 17.472c5.328 2.71 11.631.229 16.269-2.123c-1.176 4.572-5.911 5.585-8.916 6.107'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M29.099 37.115c4.376-.294 8.024-1.578 7.833-5.296'/><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='M20.27 38.702c6.771 3.834 12.505.798 13.786-2.615'/><circle cx='24' cy='24' r='21.5' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round'/></svg>"
+_domain: "\(input.subdomain).\(input.network.domain)"
+
+images: {
+ softserve: {
+ repository: "charmcli"
+ name: "soft-serve"
+ tag: "v0.7.1"
+ pullPolicy: "IfNotPresent"
+ }
+ dodoApp: {
+ repository: "giolekva"
+ name: "pcloud-installer"
+ tag: "latest"
+ pullPolicy: "Always"
+ }
+}
+
+charts: {
+ softserve: {
+ chart: "charts/soft-serve"
+ sourceRef: {
+ kind: "GitRepository"
+ name: "pcloud"
+ namespace: global.pcloudEnvName
+ }
+ }
+ dodoApp: {
+ chart: "charts/dodo-app"
+ sourceRef: {
+ kind: "GitRepository"
+ name: "pcloud"
+ namespace: global.pcloudEnvName
+ }
+ }
+}
+
+portForward: [#PortForward & {
+ allocator: input.network.allocatePortAddr
+ sourcePort: input.sshPort
+ // TODO(gio): namespace part must be populated by app manager. Otherwise
+ // third-party app developer might point to a service from different namespace.
+ targetService: "\(release.namespace)/soft-serve"
+ targetPort: 22
+}]
+
+helm: {
+ softserve: {
+ chart: charts.softserve
+ values: {
+ serviceType: "ClusterIP"
+ addressPool: ""
+ reservedIP: ""
+ adminKey: strings.Join([input.adminKey, input.fluxKeys.public, input.dAppKeys.public], "\n")
+ privateKey: input.ssKeys.private
+ publicKey: input.ssKeys.public
+ ingress: {
+ enabled: false
+ }
+ image: {
+ repository: images.softserve.fullName
+ tag: images.softserve.tag
+ pullPolicy: images.softserve.pullPolicy
+ }
+ }
+ }
+ "dodo-app": {
+ chart: charts.dodoApp
+ values: {
+ image: {
+ repository: images.dodoApp.fullName
+ tag: images.dodoApp.tag
+ pullPolicy: images.dodoApp.pullPolicy
+ }
+ repoAddr: "soft-serve.\(release.namespace).svc.cluster.local:22"
+ sshPrivateKey: base64.Encode(null, input.dAppKeys.private)
+ self: "dodo-app.\(release.namespace).svc.cluster.local"
+ namespace: release.namespace
+ envConfig: base64.Encode(null, json.Marshal(global))
+ }
+ }
+}
+
+resources: {
+ "config-kustomization": {
+ apiVersion: "kustomize.toolkit.fluxcd.io/v1"
+ kind: "Kustomization"
+ metadata: {
+ name: "app"
+ namespace: release.namespace
+ }
+ spec: {
+ interval: "1m"
+ path: "./.dodo"
+ sourceRef: {
+ kind: "GitRepository"
+ name: "app"
+ namespace: release.namespace
+ }
+ prune: true
+ }
+ }
+ "config-secret": {
+ apiVersion: "v1"
+ kind: "Secret"
+ type: "Opaque"
+ metadata: {
+ name: "app"
+ namespace: release.namespace
+ }
+ data: {
+ identity: base64.Encode(null, input.fluxKeys.private)
+ "identity.pub": base64.Encode(null, input.fluxKeys.public)
+ known_hosts: base64.Encode(null, "soft-serve.\(release.namespace).svc.cluster.local \(input.ssKeys.public)")
+ }
+ }
+ "config-source": {
+ apiVersion: "source.toolkit.fluxcd.io/v1"
+ kind: "GitRepository"
+ metadata: {
+ name: "app"
+ namespace: release.namespace
+ }
+ spec: {
+ interval: "1m0s"
+ ref: branch: "dodo"
+ secretRef: name: "app"
+ timeout: "60s"
+ url: "ssh://soft-serve.\(release.namespace).svc.cluster.local:22/app"
+ }
+ }
+}
+
+help: [{
+ title: "How to use"
+ contents: """
+ Clone: git clone ssh://\(_domain):\(input.sshPort)/app
+ """
+}]
diff --git a/core/installer/values-tmpl/soft-serve.cue b/core/installer/values-tmpl/soft-serve.cue
index b99430a..1bae24f 100644
--- a/core/installer/values-tmpl/soft-serve.cue
+++ b/core/installer/values-tmpl/soft-serve.cue
@@ -5,7 +5,7 @@
adminKey: string @name(Admin SSH Public Key)
}
-_domain: "\(input.subdomain).\(global.privateDomain)"
+_domain: "\(input.subdomain).\(input.network.domain)"
name: "Soft-Serve"
namespace: "app-soft-serve"
@@ -35,7 +35,7 @@
}
ingress: {
- gerrit: {
+ gerrit: { // TODO(gio): rename to soft-serve
auth: enabled: false
network: input.network
subdomain: input.subdomain
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
new file mode 100644
index 0000000..5eb2f58
--- /dev/null
+++ b/core/installer/welcome/dodo_app.go
@@ -0,0 +1,122 @@
+package welcome
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/giolekva/pcloud/core/installer"
+ "github.com/giolekva/pcloud/core/installer/soft"
+)
+
+type DodoAppServer struct {
+ port int
+ sshKey string
+ client soft.Client
+ namespace string
+ env installer.EnvConfig
+ workers map[string]struct{}
+}
+
+func NewDodoAppServer(
+ port int,
+ sshKey string,
+ client soft.Client,
+ namespace string,
+ env installer.EnvConfig,
+) *DodoAppServer {
+ return &DodoAppServer{
+ port,
+ sshKey,
+ client,
+ namespace,
+ env,
+ map[string]struct{}{},
+ }
+}
+
+func (s *DodoAppServer) Start() error {
+ http.HandleFunc("/update", s.handleUpdate)
+ http.HandleFunc("/register-worker", s.handleRegisterWorker)
+ return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
+}
+
+type updateReq struct {
+ Ref string `json:"ref"`
+}
+
+func (s *DodoAppServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
+ fmt.Println("update")
+ var req updateReq
+ var contents strings.Builder
+ io.Copy(&contents, r.Body)
+ c := contents.String()
+ fmt.Println(c)
+ if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
+ fmt.Println(err)
+ return
+ }
+ if req.Ref != "refs/heads/master" {
+ return
+ }
+ go func() {
+ time.Sleep(20 * time.Second)
+ if err := UpdateDodoApp(s.client, s.namespace, s.sshKey, &s.env); err != nil {
+ fmt.Println(err)
+ }
+ }()
+ for addr, _ := range s.workers {
+ go func() {
+ // TODO(gio): make port configurable
+ http.Get(fmt.Sprintf("http://%s:3000/update", addr))
+ }()
+ }
+}
+
+type registerWorkerReq struct {
+ Address string `json:"address"`
+}
+
+func (s *DodoAppServer) handleRegisterWorker(w http.ResponseWriter, r *http.Request) {
+ var req registerWorkerReq
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ s.workers[req.Address] = struct{}{}
+ fmt.Printf("registered worker: %s\n", req.Address)
+}
+
+func UpdateDodoApp(client soft.Client, namespace string, sshKey string, env *installer.EnvConfig) error {
+ repo, err := client.GetRepo("app")
+ if err != nil {
+ return err
+ }
+ nsCreator := installer.NewNoOpNamespaceCreator()
+ if err != nil {
+ return err
+ }
+ m, err := installer.NewAppManager(repo, nsCreator, "/.dodo")
+ if err != nil {
+ return err
+ }
+ appCfg, err := soft.ReadFile(repo, "app.cue")
+ fmt.Println(string(appCfg))
+ if err != nil {
+ return err
+ }
+ app, err := installer.NewDodoApp(appCfg)
+ if err != nil {
+ return err
+ }
+ if _, err := m.Install(app, "app", "/.dodo/app", namespace, map[string]any{
+ "repoAddr": repo.FullAddress(),
+ "sshPrivateKey": sshKey,
+ }, installer.WithConfig(env), installer.WithBranch("dodo")); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index e4cee83..0803e64 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -59,7 +59,7 @@
return nil
}
-func (r mockRepoIO) CommitAndPush(message string) error {
+func (r mockRepoIO) CommitAndPush(message string, opts ...soft.PushOption) error {
r.t.Logf("Commit and push: %s", message)
return nil
}
@@ -128,6 +128,10 @@
return nil
}
+func (f fakeSoftServeClient) AddWebhook(repo, url string, opts ...string) error {
+ return nil
+}
+
type fakeClientGetter struct {
t *testing.T
envFS billy.Filesystem