diff --git a/Makefile b/Makefile
index 0ce9dd3c..02e55059 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,10 @@ date_time := $(shell date +%Y%m%d)
ci_tag := $(citag)
export kube_api_host := $(shell kubectl get svc kubernetes -n default -o jsonpath='{.spec.clusterIP}')
export commit_id := $(shell git rev-parse --short HEAD)
-export branch_name := $(shell git branch -r --contains | head -1 | sed -E -e "s%(HEAD ->|origin|upstream)/?%%g" | xargs | tr '/' '-' )
+
+# read gitlab-ci branch tag first, git command for developer environment
+export branch_name := $(or $(CI_COMMIT_BRANCH),$(shell git branch --show-current))
+export branch_name := $(shell echo $(branch_name) | tr "/" "-")
export _branch_prefix := $(shell echo $(branch_name) | sed 's/-.*//')
ifneq (,$(filter $(_branch_prefix), test sprint))
@@ -76,4 +79,8 @@ pull: ## 拉取最新镜像
mountFiles:
mkdir -p /root/.config/helm
kubectl get cm -n cne-system qucheng-files -o jsonpath='{.data.repositories\.yaml}' > /root/.config/helm/repositories.yaml.dev
- sed -r -e "s%(\s+server:\s+https://).*(:6443)%\1$(kube_api_host)\2%" ~/.kube/config > /root/.kube/config.dev
+ @sed -r -e "s%(\s+server:\s+https://).*(:6443)%\1$(kube_api_host)\2%" ~/.kube/config > /root/.kube/config.dev
+
+debug:
+ @echo $(branch_name) "123"
+
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index 149ba452..ab7a53db 100644
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -26,6 +26,7 @@ func main() {
cmd.AddCommand(serve.NewCmdServe())
cmd.AddCommand(version.NewCmdVersion())
+
if err := cmd.Execute(); err != nil {
panic(err)
}
diff --git a/backend/cmd/serve/serve.go b/backend/cmd/serve/serve.go
index fee8e440..f05efc66 100644
--- a/backend/cmd/serve/serve.go
+++ b/backend/cmd/serve/serve.go
@@ -7,8 +7,13 @@ package serve
import (
"context"
"os/signal"
+ "strings"
"syscall"
+ "github.com/spf13/viper"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+
"github.com/spf13/cobra"
gins "gitlab.zcorp.cc/pangu/cne-api/internal/app/serve"
@@ -21,6 +26,16 @@ func NewCmdServe() *cobra.Command {
Short: "serve apiserver",
Run: serve,
}
+
+ viper.AutomaticEnv()
+ viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
+
+ flags := cmd.Flags()
+ flags.String(logging.FlagLogLevel, "info", "logging level")
+ viper.BindPFlag(logging.FlagLogLevel, flags.Lookup(logging.FlagLogLevel))
+
+ flags.String(constant.FlagRuntimeNamespace, "cne-system", "working namespace")
+ viper.BindPFlag(constant.FlagRuntimeNamespace, flags.Lookup(constant.FlagRuntimeNamespace))
return cmd
}
diff --git a/backend/go.mod b/backend/go.mod
index 546b3e23..0561da77 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -13,12 +13,15 @@ require (
github.com/go-sql-driver/mysql v1.5.0
github.com/google/uuid v1.3.0
github.com/imdario/mergo v0.3.12
+ github.com/kelseyhightower/envconfig v1.4.0
+ github.com/parnurzeal/gorequest v0.2.16
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.1
github.com/robfig/cron/v3 v3.0.1
github.com/sethvargo/go-envconfig v0.6.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.4.0
+ github.com/spf13/viper v1.8.1
github.com/swaggo/gin-swagger v1.4.2
github.com/swaggo/swag v1.8.1
github.com/vmware-tanzu/velero v1.9.0
@@ -62,6 +65,7 @@ require (
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.13.0 // indirect
+ github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
@@ -82,6 +86,7 @@ require (
github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmoiron/sqlx v1.3.4 // indirect
@@ -93,6 +98,7 @@ require (
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.4 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+ github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
@@ -100,6 +106,7 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
+ github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
@@ -111,6 +118,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
+ github.com/pelletier/go-toml v1.9.3 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
@@ -119,9 +127,12 @@ require (
github.com/rubenv/sql-migrate v1.1.1 // indirect
github.com/russross/blackfriday v1.5.2 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
+ github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
+ github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.7.1 // indirect
+ github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
@@ -142,6 +153,7 @@ require (
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apiextensions-apiserver v0.24.2 // indirect
k8s.io/apiserver v0.24.2 // indirect
@@ -151,6 +163,7 @@ require (
k8s.io/klog/v2 v2.70.1 // indirect
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e // indirect
+ moul.io/http2curl v1.0.0 // indirect
oras.land/oras-go v1.1.1 // indirect
sigs.k8s.io/controller-runtime v0.12.3 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
diff --git a/backend/go.sum b/backend/go.sum
index c33e03bc..60c60c67 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -214,6 +214,7 @@ github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
+github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
@@ -375,6 +376,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
@@ -404,6 +406,7 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -437,11 +440,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
+github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
+github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -477,6 +483,7 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -522,6 +529,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -576,8 +584,11 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
+github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
+github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -655,13 +666,16 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -672,12 +686,14 @@ github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t6
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -692,6 +708,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/gin-swagger v1.4.2 h1:qDs1YrBOTnurDG/JVMc8678KhoS1B1okQGPtIqVz4YU=
@@ -1194,6 +1211,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
@@ -1266,6 +1284,8 @@ k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e h1:W1yba+Bpkwb5BatGKZALQ1yylhwnuD6CkYmrTibyLDM=
k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
+moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
oras.land/oras-go v1.1.1 h1:gI00ftziRivKXaw1BdMeEoIA4uBgga33iVlOsEwefFs=
oras.land/oras-go v1.1.1/go.mod h1:n2TE1ummt9MUyprGhT+Q7kGZUF4kVUpYysPFxeV2IpQ=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/backend/internal/app/model/app.go b/backend/internal/app/model/app.go
index 1ddd4d3b..9a58dfaf 100644
--- a/backend/internal/app/model/app.go
+++ b/backend/internal/app/model/app.go
@@ -4,19 +4,24 @@
package model
-type AppModel struct {
+type ResourceModel struct {
QueryNamespace
Name string `form:"name" json:"name" binding:"required"`
}
+type AppModel struct {
+ ResourceModel
+}
+
type AppCreateOrUpdateModel struct {
AppModel
- Channel string `json:"channel"`
- Chart string `json:"chart" binding:"required"`
- Version string `json:"version" binding:"version_format"`
- Username string `json:"username,omitempty"`
- Settings []stringSetting `json:"settings"`
- SettingsMap map[string]interface{} `json:"settings_map"`
+ Channel string `json:"channel"`
+ Chart string `json:"chart" binding:"required"`
+ Version string `json:"version" binding:"version_format"`
+ Username string `json:"username,omitempty"`
+ Settings []stringSetting `json:"settings"`
+ SettingsMap map[string]interface{} `json:"settings_map"`
+ SettingsSnippets []string `json:"settings_snippets"`
}
type stringSetting struct {
diff --git a/backend/internal/app/model/component.go b/backend/internal/app/model/component.go
index b2e702af..bf56c4fd 100644
--- a/backend/internal/app/model/component.go
+++ b/backend/internal/app/model/component.go
@@ -34,6 +34,7 @@ type ComponentDbService struct {
DbType string `json:"db_type"`
Alias string `json:"alias"`
Status string `json:"status"`
+ Ready bool `json:"ready"`
}
type ComponentDbServiceDetail struct {
@@ -55,3 +56,10 @@ type DbValidation struct {
User bool `json:"user"`
Database bool `json:"database"`
}
+
+type ComponentDb struct {
+ ComponentBase
+ DbType string `json:"db_type"`
+ DbName string `json:"db_name"`
+ Ready bool `json:"ready"`
+}
diff --git a/backend/internal/app/model/snippet.go b/backend/internal/app/model/snippet.go
new file mode 100644
index 00000000..7e619000
--- /dev/null
+++ b/backend/internal/app/model/snippet.go
@@ -0,0 +1,9 @@
+package model
+
+type SnippetConfig struct {
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+ Category string `json:"category,omitempty"`
+ Content string `json:"content"`
+ AutoImport bool `json:"auto-import,omitempty"`
+}
diff --git a/backend/internal/app/router/app.go b/backend/internal/app/router/app.go
index 4b73a5f7..69a373ee 100644
--- a/backend/internal/app/router/app.go
+++ b/backend/internal/app/router/app.go
@@ -9,6 +9,9 @@ import (
"net/http"
"sync"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app/instance"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/db/manage"
+
"github.com/sirupsen/logrus"
"gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
@@ -18,7 +21,6 @@ import (
"gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service"
- "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app"
)
// AppInstall 安装接口
@@ -39,7 +41,7 @@ func AppInstall(c *gin.Context) {
ctx = c.Request.Context()
err error
body model.AppCreateOrUpdateModel
- i *app.Instance
+ i *instance.Instance
)
if err = c.ShouldBindJSON(&body); err != nil {
renderError(c, http.StatusBadRequest, err)
@@ -59,7 +61,9 @@ func AppInstall(c *gin.Context) {
return
}
- if err = service.Apps(ctx, body.Cluster, body.Namespace).Install(body.Name, body); err != nil {
+ snippetSettings := MergeSnippetConfigs(ctx, body.Namespace, body.SettingsSnippets, logger)
+
+ if err = service.Apps(ctx, body.Cluster, body.Namespace).Install(body.Name, body, snippetSettings); err != nil {
logger.WithError(err).Error("install app failed")
renderError(c, http.StatusInternalServerError, err)
return
@@ -87,7 +91,7 @@ func AppUnInstall(c *gin.Context) {
body model.AppModel
)
- ctx, i, code, err := LookupApp(c, &body)
+ _, i, code, err := LookupApp(c, &body)
if err != nil {
renderError(c, code, err)
return
@@ -95,7 +99,7 @@ func AppUnInstall(c *gin.Context) {
logger := i.GetLogger()
- if err = service.Apps(ctx, body.Cluster, body.Namespace).UnInstall(body.Name); err != nil {
+ if err = i.Uninstall(); err != nil {
logger.WithError(err).Error("uninstall app failed")
renderError(c, http.StatusInternalServerError, err)
return
@@ -195,7 +199,7 @@ func AppPatchSettings(c *gin.Context) {
body model.AppCreateOrUpdateModel
)
- _, i, code, err := LookupApp(c, &body)
+ ctx, i, code, err := LookupApp(c, &body)
if err != nil {
renderError(c, code, err)
return
@@ -203,7 +207,9 @@ func AppPatchSettings(c *gin.Context) {
logger := i.GetLogger()
- err = i.PatchSettings(body.Chart, body)
+ snippetSettings := MergeSnippetConfigs(ctx, body.Namespace, body.SettingsSnippets, logger)
+
+ err = i.PatchSettings(body.Chart, body, snippetSettings)
if err != nil {
logger.WithError(err).Error(errPatchAppFailed)
renderError(c, http.StatusInternalServerError, errors.New(errPatchAppFailed))
@@ -416,6 +422,76 @@ func AppAccountInfo(c *gin.Context) {
renderJson(c, http.StatusOK, data)
}
+func AppDbList(c *gin.Context) {
+ var (
+ query model.AppModel
+ )
+
+ _, i, code, err := LookupApp(c, &query)
+ if err != nil {
+ renderError(c, code, err)
+ return
+ }
+
+ logger := i.GetLogger()
+ dbs := i.GetDbList()
+ var data []model.ComponentDb
+ for _, item := range dbs {
+ dbsvc, dbMeta, err := manage.ParseDB(i.Ctx, i.Ks.Store, item)
+ if err != nil {
+ logger.WithError(err).Errorf("parse db %s failed", item.Name)
+ continue
+ }
+ d := model.ComponentDb{
+ ComponentBase: model.ComponentBase{Name: item.Name, NameSpace: item.Namespace},
+ DbType: string(dbsvc.DbType()),
+ DbName: dbMeta.Name,
+ Ready: item.Status.Ready,
+ }
+ data = append(data, d)
+ }
+ renderJson(c, http.StatusOK, data)
+}
+
+func AppDbDetails(c *gin.Context) {
+ var (
+ query struct {
+ model.AppModel
+ Db string `form:"db" binding:"required"`
+ }
+ )
+
+ _, i, code, err := LookupApp(c, &query)
+ if err != nil {
+ renderError(c, code, err)
+ return
+ }
+
+ logger := i.GetLogger()
+ db, err := i.Ks.Store.GetDb(query.Namespace, query.Db)
+ if err != nil {
+ logger.WithError(err).Error("get db failed")
+ renderError(c, http.StatusInternalServerError, err)
+ return
+ }
+
+ dbsvc, dbMeta, err := manage.ParseDB(i.Ctx, i.Ks.Store, db)
+ if err != nil {
+ logger.WithError(err).Errorf("parse db %s failed", query.Db)
+ renderError(c, http.StatusInternalServerError, err)
+ return
+ }
+ data := model.ComponentDbServiceDetail{
+ ComponentBase: model.ComponentBase{Name: db.Name, NameSpace: db.Namespace},
+ Host: dbsvc.ServerInfo().Host(),
+ Port: dbsvc.ServerInfo().Port(),
+ UserName: dbMeta.User,
+ Password: dbMeta.Password,
+ Database: dbMeta.Name,
+ }
+ renderJson(c, http.StatusOK, data)
+}
+
func AppTest(c *gin.Context) {
ch, err := helm.GetChart("qucheng-test/demo", "0.1.1")
diff --git a/backend/internal/app/router/app_utils.go b/backend/internal/app/router/app_utils.go
new file mode 100644
index 00000000..a0a7b422
--- /dev/null
+++ b/backend/internal/app/router/app_utils.go
@@ -0,0 +1,45 @@
+package router
+
+import (
+ "context"
+ "strings"
+
+ "github.com/imdario/mergo"
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/viper"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/snippet"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+)
+
+func MergeSnippetConfigs(ctx context.Context, namespace string, snippetNames []string, logger logrus.FieldLogger) map[string]interface{} {
+ var data map[string]interface{}
+ for _, name := range snippetNames {
+ logger.Debugf("try to parse snippet '%s'", name)
+ if !strings.HasPrefix(name, snippet.NamePrefix) {
+ name = snippet.NamePrefix + name
+ logger.Debugf("use internal snippet name '%s'", name)
+ }
+
+ s, err := service.Snippets(ctx, "").Get(namespace, name)
+ if err != nil {
+ logger.WithError(err).Debugf("get snippet '%s' from namespace '%s' failed", name, namespace)
+ runtimeNs := viper.GetString(constant.FlagRuntimeNamespace)
+ logger.Infof("try to load snippet '%s' from namespace '%s'", name, runtimeNs)
+ s, err = service.Snippets(ctx, "").Get(name, runtimeNs)
+ if err != nil {
+ logger.WithError(err).Errorf("failed to get snippet '%s'", name)
+ }
+ }
+ if s == nil {
+ continue
+ }
+
+ err = mergo.Merge(&data, s.Values(), mergo.WithOverride)
+ if err != nil {
+ logger.WithError(err).WithField("snippet", name).Error("merge snippet config failed")
+ }
+ }
+ return data
+}
diff --git a/backend/internal/app/router/router.go b/backend/internal/app/router/router.go
index 474168c9..4407f422 100644
--- a/backend/internal/app/router/router.go
+++ b/backend/internal/app/router/router.go
@@ -57,6 +57,8 @@ func Config(r *gin.Engine) {
api.GET("/app/metric", AppMetric)
api.GET("/app/pvc", AppPvcList)
api.GET("/app/account", AppAccountInfo)
+ api.GET("/app/dbs", AppDbList)
+ api.GET("/app/dbs/detail", AppDbDetails)
api.GET("/test", AppTest)
@@ -91,6 +93,11 @@ func Config(r *gin.Engine) {
api.POST("/system/update", SystemUpdate)
api.GET("/system/app-full-list", FindAllApps)
+
+ api.GET("/snippet", ListSnippets)
+ api.POST("/snippet/add", CreateSnippet)
+ api.POST("/snippet/update", UpdateSnippet)
+ api.POST("/snippet/remove", RemoveSnippet)
}
r.NoMethod(func(c *gin.Context) {
diff --git a/backend/internal/app/router/snippet.go b/backend/internal/app/router/snippet.go
new file mode 100644
index 00000000..a4294647
--- /dev/null
+++ b/backend/internal/app/router/snippet.go
@@ -0,0 +1,113 @@
+package router
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/snippet"
+)
+
+func ListSnippets(c *gin.Context) {
+ var (
+ err error
+ ctx = c.Request.Context()
+ )
+
+ logger := getLogger(ctx)
+ namespace := c.DefaultQuery("namespace", "")
+
+ data, err := service.Snippets(ctx, "").List(namespace)
+ if err != nil {
+ logger.WithError(err).Error("failed to list snippets")
+ renderError(c, http.StatusInternalServerError, err)
+ return
+ }
+
+ renderJson(c, http.StatusOK, data)
+}
+
+func CreateSnippet(c *gin.Context) {
+ var (
+ ctx = c.Request.Context()
+ err error
+ body model.SnippetConfig
+ )
+
+ logger := getLogger(ctx)
+ if err = c.ShouldBindJSON(&body); err != nil {
+ logger.WithError(err).Error("bind json failed")
+ renderError(c, http.StatusOK, err)
+ return
+ }
+ logger.Debugf("receive snippet create request: %+v", body)
+
+ if !strings.HasPrefix(body.Name, snippet.NamePrefix) {
+ e := fmt.Errorf("snippet name should start with 'snippet-'")
+ logger.WithError(err).Error("invalid post data")
+ renderError(c, http.StatusBadRequest, e)
+ return
+ }
+
+ err = service.Snippets(ctx, "").Create(body.Name, body.Namespace, body.Content)
+ if err != nil {
+ logger.WithError(err).Error("failed to create snippet")
+ renderError(c, http.StatusInternalServerError, err)
+ return
+ }
+
+ renderSuccess(c, http.StatusOK)
+}
+
+func UpdateSnippet(c *gin.Context) {
+ var (
+ ctx = c.Request.Context()
+ err error
+ body model.SnippetConfig
+ )
+
+ logger := getLogger(ctx)
+ if err = c.ShouldBindJSON(&body); err != nil {
+ logger.WithError(err).Error("bind json failed")
+ renderError(c, http.StatusOK, err)
+ return
+ }
+ logger.Debugf("receive snippet update request: %+v", body)
+
+ err = service.Snippets(ctx, "").Update(body.Name, body.Namespace, body.Content)
+ if err != nil {
+ logger.WithError(err).Error("failed to create snippet")
+ renderError(c, http.StatusInternalServerError, err)
+ return
+ }
+
+ renderSuccess(c, http.StatusOK)
+}
+
+func RemoveSnippet(c *gin.Context) {
+ var (
+ ctx = c.Request.Context()
+ err error
+ body model.ResourceModel
+ )
+
+ logger := getLogger(ctx)
+ if err = c.ShouldBindJSON(&body); err != nil {
+ logger.WithError(err).Error("bind json failed")
+ renderError(c, http.StatusOK, err)
+ return
+ }
+
+ err = service.Snippets(ctx, body.Cluster).Remove(body.Name, body.Namespace)
+ if err != nil {
+ logger.WithError(err).Error("failed to remove snippet")
+ renderError(c, http.StatusInternalServerError, err)
+ return
+ }
+
+ renderSuccess(c, http.StatusOK)
+}
diff --git a/backend/internal/app/router/system.go b/backend/internal/app/router/system.go
index 2bf60932..0e58d487 100644
--- a/backend/internal/app/router/system.go
+++ b/backend/internal/app/router/system.go
@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service"
@@ -24,16 +25,19 @@ func SystemUpdate(c *gin.Context) {
return
}
- qcApp, err := service.Apps(ctx, "", constant.DefaultRuntimeNamespace).GetApp("qucheng")
+ runtimeNs := viper.GetString(constant.FlagRuntimeNamespace)
+
+ qcApp, err := service.Apps(ctx, "", runtimeNs).GetApp("qucheng")
if err != nil {
logger.WithError(err).Error("get qucheng app failed")
renderError(c, http.StatusInternalServerError, err)
return
}
+ blankSnippet := make(map[string]interface{})
if err = qcApp.PatchSettings(qcApp.ChartName, model.AppCreateOrUpdateModel{
Version: body.Version, Channel: body.Channel,
- }); err != nil {
+ }, blankSnippet); err != nil {
logger.WithError(err).WithField("channel", body.Channel).Errorf("update qucheng chart to version %s failed", body.Version)
renderError(c, http.StatusInternalServerError, err)
return
@@ -41,7 +45,7 @@ func SystemUpdate(c *gin.Context) {
logger.WithField("channel", body.Channel).Infof("update qucheng chart to version %s success", body.Version)
- opApp, err := service.Apps(ctx, "", constant.DefaultRuntimeNamespace).GetApp("cne-operator")
+ opApp, err := service.Apps(ctx, "", runtimeNs).GetApp("cne-operator")
if err != nil {
logger.WithError(err).Error("get cne-operator app failed")
renderError(c, http.StatusInternalServerError, err)
@@ -50,7 +54,7 @@ func SystemUpdate(c *gin.Context) {
if err = opApp.PatchSettings(opApp.ChartName, model.AppCreateOrUpdateModel{
Version: "latest", Channel: body.Channel,
- }); err != nil {
+ }, blankSnippet); err != nil {
logger.WithError(err).WithField("channel", body.Channel).Info("update operator chart failed")
renderError(c, http.StatusInternalServerError, err)
return
diff --git a/backend/internal/app/router/tool.go b/backend/internal/app/router/tool.go
index 3ccaaa99..74d61ef3 100644
--- a/backend/internal/app/router/tool.go
+++ b/backend/internal/app/router/tool.go
@@ -10,21 +10,22 @@ import (
"errors"
"net/http"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app/instance"
+
"github.com/sirupsen/logrus"
"github.com/gin-gonic/gin"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service"
- "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app"
"gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
)
-func LookupApp(c *gin.Context, q interface{}) (context.Context, *app.Instance, int, error) {
+func LookupApp(c *gin.Context, q interface{}) (context.Context, *instance.Instance, int, error) {
var (
ctx = c.Request.Context()
err error
- i *app.Instance
+ i *instance.Instance
)
if c.Request.Method == "POST" {
@@ -45,7 +46,7 @@ func LookupApp(c *gin.Context, q interface{}) (context.Context, *app.Instance, i
logger.WithError(err).WithFields(logrus.Fields{
"name": query.Name, "namespace": query.Namespace,
}).Error(errGetAppFailed)
- if errors.Is(err, app.ErrAppNotFound) {
+ if errors.Is(err, instance.ErrAppNotFound) {
return ctx, i, http.StatusNotFound, err
}
return ctx, i, http.StatusInternalServerError, errors.New(errGetAppStatusFailed)
diff --git a/backend/internal/app/serve/serve.go b/backend/internal/app/serve/serve.go
index 306385d1..84d78464 100644
--- a/backend/internal/app/serve/serve.go
+++ b/backend/internal/app/serve/serve.go
@@ -11,6 +11,11 @@ import (
"os"
"time"
+ "github.com/spf13/viper"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/analysis"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+
"github.com/sirupsen/logrus"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service"
@@ -31,22 +36,30 @@ func Serve(ctx context.Context, logger logrus.FieldLogger) error {
var err error
stopCh := make(chan struct{})
+ logger.Infof("Setup runtime namespace to %s", viper.GetString(constant.FlagRuntimeNamespace))
+
logger.Info("Initialize clusters")
for cluster.Init(stopCh) != nil {
- logger.Errorf("initialize failed")
+ logger.Errorf("initialize kubernetes client failed")
time.Sleep(time.Second * 10)
}
+ logger.Info("Setup analysis")
+ als := analysis.Init()
+ go als.Run(ctx)
+
logger.Info("Setup cron tasks")
_ = helm.RepoUpdate()
- defer cron.Cron.Stop()
- cron.Cron.Add("01 * * * *", func() {
+
+ cr := cron.New()
+ defer cr.Stop()
+ cr.Add("01 * * * *", func() {
err = helm.RepoUpdate()
if err != nil {
logger.WithError(err).Warning("cron helm repo update failed")
}
})
- cron.Cron.Start()
+ cr.Start()
service.Apps(ctx, "", "").Upgrade()
diff --git a/backend/internal/app/service/app/const.go b/backend/internal/app/service/app/const.go
deleted file mode 100644
index 50a58e1e..00000000
--- a/backend/internal/app/service/app/const.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (c) 2022 北京渠成软件有限公司 All rights reserved.
-// Use of this source code is governed by Z PUBLIC LICENSE 1.2 (ZPL 1.2)
-// license that can be found in the LICENSE file.
-
-package app
diff --git a/backend/internal/app/service/app/install.go b/backend/internal/app/service/app/install.go
new file mode 100644
index 00000000..f7853fab
--- /dev/null
+++ b/backend/internal/app/service/app/install.go
@@ -0,0 +1,126 @@
+package app
+
+import (
+ "io/ioutil"
+ "os"
+
+ "github.com/sirupsen/logrus"
+ "gopkg.in/yaml.v3"
+ "helm.sh/helm/v3/pkg/cli/values"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/analysis"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/utils/kvpath"
+)
+
+func (m *Manager) Install(name string, body model.AppCreateOrUpdateModel, snippetSettings map[string]interface{}) error {
+ logger := m.logger.WithFields(logrus.Fields{
+ "name": name, "namespace": body.Namespace,
+ })
+ h, err := helm.NamespaceScope(m.namespace)
+ if err != nil {
+ return err
+ }
+
+ options := &values.Options{ValueFiles: make([]string, 0)}
+
+ if len(snippetSettings) > 0 {
+ snippetValueFile, err := writeValuesFile(snippetSettings)
+ if err != nil {
+ logger.WithError(err).Error("write values file failed")
+ } else {
+ defer os.Remove(snippetValueFile)
+ options.ValueFiles = append(options.ValueFiles, snippetValueFile)
+ }
+ }
+
+ if len(body.SettingsMap) > 0 {
+ logger.Infof("user custom settingsMap is %+v", body.SettingsMap)
+ f, err := writeValuesFile(body.SettingsMap)
+ if err != nil {
+ logger.WithError(err).Error("write values file failed")
+ } else {
+ defer os.Remove(f)
+ options.ValueFiles = append(options.ValueFiles, f)
+ }
+ }
+
+ var settings = make([]string, len(body.Settings))
+ for _, s := range body.Settings {
+ settings = append(settings, s.Key+"="+s.Val)
+ }
+ options.Values = settings
+ logger.Infof("user custom settings is %+v", settings)
+
+ if err = helm.RepoUpdate(); err != nil {
+ logger.WithError(err).Error("helm update repo failed")
+ return err
+ }
+
+ feats := parseFeatures(body.SettingsMap)
+ rel, err := h.Install(name, helm.GenChart(body.Channel, body.Chart), body.Version, options)
+ if err != nil {
+ logger.WithError(err).Error("helm install failed")
+ analysis.Install(body.Chart, body.Version).WithFeatures(feats).Fail(err)
+ if _, e := h.GetRelease(name); e == nil {
+ logger.Info("recycle incomplete release")
+ _ = h.Uninstall(name)
+ }
+ return err
+ }
+ secretMeta := metav1.ObjectMeta{
+ Labels: map[string]string{
+ constant.LabelApplication: "true",
+ },
+ Annotations: map[string]string{
+ constant.AnnotationAppChannel: body.Channel,
+ },
+ }
+ if body.Username != "" {
+ secretMeta.Annotations[constant.AnnotationAppCreator] = body.Username
+ }
+ err = completeAppLabels(m.ctx, rel, m.ks, logger, secretMeta)
+ analysis.Install(body.Chart, rel.Chart.Metadata.Version).WithFeatures(feats).Success()
+ return err
+}
+
+func writeValuesFile(data map[string]interface{}) (string, error) {
+ f, err := ioutil.TempFile("/tmp", "values.******.yaml")
+ if err != nil {
+ return "", err
+ }
+ vars, err := yaml.Marshal(data)
+ if err != nil {
+ return "nil", err
+ }
+ _, err = f.Write(vars)
+ if err != nil {
+ return "nil", err
+ }
+ _ = f.Close()
+ return f.Name(), nil
+}
+
+func parseFeatures(values map[string]interface{}) []string {
+ feats := make([]string, 0)
+
+ // parse global database
+ if kvpath.Exist(values, "mysql.enabled") {
+ if !kvpath.ReadBool(values, "mysql.enabled") && kvpath.Exist(values, "mysql.auth.dbservice") {
+ feats = append(feats, "gdb")
+ }
+ } else if kvpath.Exist(values, "postgresql.enabled") {
+ if !kvpath.ReadBool(values, "postgresql.enabled") && kvpath.Exist(values, "postgresql.auth.dbservice") {
+ feats = append(feats, "gdb")
+ }
+ } else if kvpath.Exist(values, "mongodb.enabled") {
+ if !kvpath.ReadBool(values, "mongodb.enabled") && kvpath.Exist(values, "mongodb.auth.dbservice") {
+ feats = append(feats, "gdb")
+ }
+ }
+
+ return feats
+}
diff --git a/backend/internal/app/service/app/instance.go b/backend/internal/app/service/app/instance.go
index a76a7ecd..7e792fb7 100644
--- a/backend/internal/app/service/app/instance.go
+++ b/backend/internal/app/service/app/instance.go
@@ -4,433 +4,455 @@
package app
-import (
- "context"
- "fmt"
- "reflect"
- "strings"
-
- "github.com/imdario/mergo"
- "github.com/sirupsen/logrus"
-
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
- "gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
-
- "helm.sh/helm/v3/pkg/release"
- v1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/resource"
- "k8s.io/apimachinery/pkg/labels"
-
- "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
- "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app/component"
- "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
- "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
- "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/metric"
- "gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
-)
-
-type Instance struct {
- ctx context.Context
-
- clusterName string
- namespace string
- name string
-
- selector labels.Selector
-
- ks *cluster.Cluster
-
- release *release.Release
- secret *v1.Secret
-
- ChartName string
- CurrentChartVersion string
-
- logger logrus.FieldLogger
-}
-
-func newApp(ctx context.Context, am *Manager, name string) *Instance {
-
- i := &Instance{
- ctx: ctx,
- clusterName: am.clusterName, namespace: am.namespace, name: name,
- ks: am.ks,
- logger: logging.DefaultLogger().WithContext(ctx).WithFields(logrus.Fields{
- "name": name, "namespace": am.namespace,
- }),
- }
-
- i.release = i.fetchRelease()
- return i
-}
-
-func (i *Instance) prepare() error {
- i.ChartName = i.release.Chart.Metadata.Name
- i.CurrentChartVersion = i.release.Chart.Metadata.Version
-
- i.selector = labels.Set{"release": i.name}.AsSelector()
- secret, err := loadAppSecret(i.ctx, i.name, i.namespace, i.release.Version, i.ks)
- if err != nil {
- i.logger.WithError(err).Errorf("got release secret failed with resivion %d", i.release.Version)
- return err
- }
- i.secret = secret
- return nil
-}
-
-func (i *Instance) fetchRelease() *release.Release {
- getter := &ReleaseGetter{
- namespace: i.namespace,
- store: i.ks.Store,
- }
- rel, err := getter.Last(i.name)
- if err != nil {
- i.logger.WithError(err).Error("parse release failed")
- }
- return rel
-}
-
-func (i *Instance) isApp() bool {
- v, ok := i.secret.Labels[constant.LabelApplication]
- if ok && v == "true" {
- return true
- }
- return false
-}
-
-func (i *Instance) GetLogger() logrus.FieldLogger {
- return i.logger
-}
-
-func (i *Instance) getServices() ([]*v1.Service, error) {
- return i.ks.Store.ListServices(i.namespace, i.selector)
-}
-
-func (i *Instance) getComponents() *component.Components {
- components := component.NewComponents()
-
- deployments, _ := i.ks.Store.ListDeployments(i.namespace, i.selector)
-
- if len(deployments) >= 1 {
- for _, d := range deployments {
- components.Add(component.NewDeployComponent(d, i.ks))
- }
- }
-
- statefulsets, _ := i.ks.Store.ListStatefulSets(i.namespace, i.selector)
-
- if len(statefulsets) >= 1 {
- for _, s := range statefulsets {
- components.Add(component.NewStatefulsetComponent(s, i.ks))
- }
- }
-
- return components
-}
-
-func (i *Instance) ParseStatus() *model.AppRespStatus {
- var settingStopped = false
- components := i.getComponents()
-
- data := &model.AppRespStatus{
- Components: make([]model.AppRespStatusComponent, 0),
- Status: constant.AppStatusMap[constant.AppStatusUnknown],
- Version: i.CurrentChartVersion,
- Age: 0,
- }
-
- if len(components.Items()) == 0 {
- return data
- }
-
- stopped, err := i.settingParse("global.stopped")
- if err == nil {
- if stop, ok := stopped.(bool); ok {
- settingStopped = stop
- }
- }
-
- for _, c := range components.Items() {
- resC := model.AppRespStatusComponent{
- Name: c.Name(),
- Kind: c.Kind(),
- Replicas: c.Replicas(),
- StatusCode: c.Status(settingStopped),
- Status: constant.AppStatusMap[c.Status(settingStopped)],
- Age: c.Age(),
- }
- data.Components = append(data.Components, resC)
- }
-
- minStatusCode := data.Components[0].StatusCode
- maxAge := data.Components[0].Age
- for _, comp := range data.Components {
- if comp.StatusCode < minStatusCode {
- minStatusCode = comp.StatusCode
- }
-
- if comp.Age > maxAge {
- maxAge = comp.Age
- }
- }
-
- data.Status = constant.AppStatusMap[minStatusCode]
- data.Age = maxAge
- return data
-}
-
-func (i *Instance) ListIngressHosts() []string {
- var hosts []string
- ingresses, err := i.ks.Store.ListIngresses(i.namespace, i.selector)
- if err != nil {
- return hosts
- }
- for _, ing := range ingresses {
- for _, rule := range ing.Spec.Rules {
- hosts = append(hosts, rule.Host)
- }
- }
- return hosts
-}
-
-func (i *Instance) ParseNodePort() int32 {
- var nodePort int32 = 0
- services, err := i.getServices()
- if err != nil {
- return nodePort
- }
-
- for _, s := range services {
- if s.Spec.Type == v1.ServiceTypeNodePort {
- for _, p := range s.Spec.Ports {
- if p.Name == constant.ServicePortWeb || p.Name == "http" {
- nodePort = p.NodePort
- break
- }
- }
- }
- }
-
- return nodePort
-}
-
-func (i *Instance) Settings() *Settings {
- return newSettings(i)
-}
-
-func (i *Instance) settingParse(path string) (interface{}, error) {
- var err error
- var ok bool
- var node map[string]interface{}
- var data interface{}
-
- frames := strings.Split(path, ".")
- node = i.release.Config
-
- if len(frames) > 1 {
- for _, frame := range frames[0 : len(frames)-1] {
- n, ok := node[frame]
- if !ok {
- err = ErrPathParseFailed
- break
- }
- ntype := reflect.TypeOf(n)
- if ntype.Kind() != reflect.Map {
- err = ErrPathParseFailed
- break
- }
- node = n.(map[string]interface{})
- }
- if err != nil {
- return nil, err
- }
- data, ok = node[frames[len(frames)-1]]
- } else {
- data, ok = node[path]
- }
-
- if !ok {
- return nil, ErrPathParseFailed
- }
-
- return data, nil
-}
-
-func (i *Instance) GetComponents() []model.Component {
- var components []model.Component
-
- components = append(components, model.Component{Name: i.ChartName})
- deps, err := helm.ParseChartDependencies(i.release.Chart)
- if err != nil {
- return components
- }
-
- for _, dep := range deps {
- components = append(components, model.Component{
- Name: dep.Name,
- })
- }
-
- return components
-}
-
-func (i *Instance) GetSchemaCategories(component string) interface{} {
- var data []string
-
- if component == i.ChartName {
- data = helm.ParseChartCategories(i.release.Chart)
- } else {
- var exist = false
- for _, dep := range i.release.Chart.Lock.Dependencies {
- if dep.Name == component {
- exist = true
- break
- }
- }
-
- if exist {
- ch, err := helm.GetChart(genChart("test", i.ChartName), i.CurrentChartVersion)
- if err != nil {
- return data
- }
-
- for _, dep := range ch.Dependencies() {
- if dep.Name() == component {
- data = helm.ParseChartCategories(dep)
- break
- }
- }
- }
- }
-
- return data
-}
-
-func (i *Instance) GetSchema(component, category string) string {
- var data string
-
- if component == i.ChartName {
- jbody, err := helm.ReadSchemaFromChart(i.release.Chart, category, "test")
- i.logger.Debugf("get schema content: %s", string(jbody))
- if err != nil {
- i.logger.WithError(err).Error("get schema failed")
- return ""
- }
- data = string(jbody)
- } else {
- ch, err := helm.GetChart(genChart("test", i.ChartName), i.CurrentChartVersion)
- if err != nil {
- return ""
- }
- for _, dep := range ch.Dependencies() {
- if dep.Name() == component {
- jbody, err := helm.ReadSchemaFromChart(dep, category, "test")
- if err != nil {
- return ""
- }
- data = string(jbody)
- }
- }
- }
- return data
-}
-
-func (i *Instance) GetPvcList() []model.AppRespPvc {
- var result []model.AppRespPvc
- pvcList, err := i.ks.Clients.Base.CoreV1().PersistentVolumeClaims(i.namespace).List(i.ctx, metav1.ListOptions{LabelSelector: i.selector.String()})
- if err != nil {
- i.logger.WithError(err).Error("list pvc failed")
- return result
- }
- for _, pvc := range pvcList.Items {
- quantity := pvc.Spec.Resources.Requests[v1.ResourceStorage]
- p := model.AppRespPvc{
- Name: pvc.Name, VolumeName: pvc.Spec.VolumeName, AccessModes: pvc.Spec.AccessModes,
- Size: quantity.AsApproximateFloat64(),
- Path: fmt.Sprintf("%s-%s-%s", pvc.Namespace, pvc.Name, pvc.Spec.VolumeName),
- }
- if pvc.Spec.StorageClassName != nil {
- p.StorageClassName = *pvc.Spec.StorageClassName
- }
- result = append(result, p)
- }
- return result
-}
-
-func (i *Instance) GetAccountInfo() map[string]string {
- data := map[string]string{
- "username": "", "password": "",
- }
-
- values := i.release.Chart.Values
- mergo.Merge(&values, i.release.Config, mergo.WithOverwriteWithEmptyValue)
-
- if auth, ok := values["auth"]; ok {
- username := lookupFields(auth.(map[string]interface{}), "username", "user")
- password := lookupFields(auth.(map[string]interface{}), "password", "passwd")
- data["username"] = username
- data["password"] = password
- }
-
- return data
-}
-
-func (i *Instance) GetMetrics() *model.AppMetric {
- metrics := i.ks.Metric.ListPodMetrics(i.ctx, i.namespace, i.selector)
- pods, _ := i.ks.Store.ListPods(i.namespace, i.selector)
-
- var usage metric.Res
- var limit metric.Res
-
- sumPodUsage(&usage, metrics)
- sumPodLimit(&limit, pods)
-
- memUsage, _ := usage.Memory.AsInt64()
- memLimit, _ := limit.Memory.AsInt64()
-
- data := model.AppMetric{
- Cpu: model.ResourceCpu{
- Usage: usage.Cpu.AsApproximateFloat64(), Limit: limit.Cpu.AsApproximateFloat64(),
- },
- Memory: model.ResourceMemory{
- Usage: memUsage, Limit: memLimit,
- },
- }
- return &data
-}
-
-func sumPodUsage(dst *metric.Res, metrics []*metric.Res) {
- dst.Cpu = resource.NewQuantity(0, resource.DecimalExponent)
- dst.Memory = resource.NewQuantity(0, resource.DecimalExponent)
-
- for _, m := range metrics {
- dst.Cpu.Add(m.Cpu.DeepCopy())
- dst.Memory.Add(m.Memory.DeepCopy())
- }
-}
-
-func sumPodLimit(dst *metric.Res, pods []*v1.Pod) {
- dst.Cpu = resource.NewQuantity(0, resource.DecimalExponent)
- dst.Memory = resource.NewQuantity(0, resource.DecimalExponent)
-
- for _, pod := range pods {
- for _, ctn := range pod.Spec.Containers {
- l := ctn.Resources.Limits
- dst.Cpu.Add(*l.Cpu())
- dst.Memory.Add(*l.Memory())
- }
- }
-}
-
-func lookupFields(m map[string]interface{}, names ...string) string {
- for _, name := range names {
- if v, ok := m[name]; ok {
- refv := reflect.ValueOf(v)
- if refv.Kind() == reflect.String {
- return v.(string)
- }
- }
- }
- return ""
-}
+//import (
+// "context"
+// "fmt"
+// "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/analysis"
+// "reflect"
+// "strings"
+// "time"
+//
+// "github.com/imdario/mergo"
+// "github.com/sirupsen/logrus"
+//
+// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+//
+// "gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
+//
+// "helm.sh/helm/v3/pkg/release"
+// v1 "k8s.io/api/core/v1"
+// "k8s.io/apimachinery/pkg/api/resource"
+// "k8s.io/apimachinery/pkg/labels"
+//
+// "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+// "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app/component"
+// "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+// "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
+// "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/metric"
+// "gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
+//)
+//
+//type Instance struct {
+// ctx context.Context
+//
+// clusterName string
+// namespace string
+// name string
+//
+// selector labels.Selector
+//
+// ks *cluster.Cluster
+//
+// release *release.Release
+// secret *v1.Secret
+//
+// ChartName string
+// CurrentChartVersion string
+//
+// logger logrus.FieldLogger
+//}
+//
+//func newApp(ctx context.Context, am *Manager, name string) *Instance {
+//
+// i := &Instance{
+// ctx: ctx,
+// clusterName: am.clusterName, namespace: am.namespace, name: name,
+// ks: am.ks,
+// logger: logging.DefaultLogger().WithContext(ctx).WithFields(logrus.Fields{
+// "name": name, "namespace": am.namespace,
+// }),
+// }
+//
+// i.release = i.fetchRelease()
+// return i
+//}
+//
+//func (i *Instance) prepare() error {
+// i.ChartName = i.release.Chart.Metadata.Name
+// i.CurrentChartVersion = i.release.Chart.Metadata.Version
+//
+// i.selector = labels.Set{"release": i.name}.AsSelector()
+// secret, err := loadAppSecret(i.ctx, i.name, i.namespace, i.release.Version, i.ks)
+// if err != nil {
+// i.logger.WithError(err).Errorf("got release secret failed with resivion %d", i.release.Version)
+// return err
+// }
+// i.secret = secret
+// return nil
+//}
+//
+//func (i *Instance) fetchRelease() *release.Release {
+// getter := &ReleaseGetter{
+// namespace: i.namespace,
+// store: i.ks.Store,
+// }
+// rel, err := getter.Last(i.name)
+// if err != nil {
+// i.logger.WithError(err).Error("parse release failed")
+// }
+// return rel
+//}
+//
+//func (i *Instance) isApp() bool {
+// v, ok := i.secret.Labels[constant.LabelApplication]
+// if ok && v == "true" {
+// return true
+// }
+// return false
+//}
+//
+//func (i *Instance) GetLogger() logrus.FieldLogger {
+// return i.logger
+//}
+//
+//func (i *Instance) getServices() ([]*v1.Service, error) {
+// return i.ks.Store.ListServices(i.namespace, i.selector)
+//}
+//
+//func (i *Instance) getComponents() *component.Components {
+// components := component.NewComponents()
+//
+// deployments, _ := i.ks.Store.ListDeployments(i.namespace, i.selector)
+//
+// if len(deployments) >= 1 {
+// for _, d := range deployments {
+// components.Add(component.NewDeployComponent(d, i.ks))
+// }
+// }
+//
+// statefulsets, _ := i.ks.Store.ListStatefulSets(i.namespace, i.selector)
+//
+// if len(statefulsets) >= 1 {
+// for _, s := range statefulsets {
+// components.Add(component.NewStatefulsetComponent(s, i.ks))
+// }
+// }
+//
+// return components
+//}
+//
+//func (i *Instance) ParseStatus() *model.AppRespStatus {
+// var settingStopped = false
+// components := i.getComponents()
+//
+// data := &model.AppRespStatus{
+// Components: make([]model.AppRespStatusComponent, 0),
+// Status: constant.AppStatusMap[constant.AppStatusUnknown],
+// Version: i.CurrentChartVersion,
+// Age: 0,
+// }
+//
+// if len(components.Items()) == 0 {
+// return data
+// }
+//
+// stopped, err := i.settingParse("global.stopped")
+// if err == nil {
+// if stop, ok := stopped.(bool); ok {
+// settingStopped = stop
+// }
+// }
+//
+// for _, c := range components.Items() {
+// resC := model.AppRespStatusComponent{
+// Name: c.Name(),
+// Kind: c.Kind(),
+// Replicas: c.Replicas(),
+// StatusCode: c.Status(settingStopped),
+// Status: constant.AppStatusMap[c.Status(settingStopped)],
+// Age: c.Age(),
+// }
+// data.Components = append(data.Components, resC)
+// }
+//
+// minStatusCode := data.Components[0].StatusCode
+// maxAge := data.Components[0].Age
+// for _, comp := range data.Components {
+// if comp.StatusCode < minStatusCode {
+// minStatusCode = comp.StatusCode
+// }
+//
+// if comp.Age > maxAge {
+// maxAge = comp.Age
+// }
+// }
+//
+// data.Status = constant.AppStatusMap[minStatusCode]
+// data.Age = maxAge
+// return data
+//}
+//
+//func (i *Instance) ListIngressHosts() []string {
+// var hosts []string
+// ingresses, err := i.ks.Store.ListIngresses(i.namespace, i.selector)
+// if err != nil {
+// return hosts
+// }
+// for _, ing := range ingresses {
+// for _, rule := range ing.Spec.Rules {
+// hosts = append(hosts, rule.Host)
+// }
+// }
+// return hosts
+//}
+//
+//func (i *Instance) ParseNodePort() int32 {
+// var nodePort int32 = 0
+// services, err := i.getServices()
+// if err != nil {
+// return nodePort
+// }
+//
+// for _, s := range services {
+// if s.Spec.Type == v1.ServiceTypeNodePort {
+// for _, p := range s.Spec.Ports {
+// if p.Name == constant.ServicePortWeb || p.Name == "http" {
+// nodePort = p.NodePort
+// break
+// }
+// }
+// }
+// }
+//
+// return nodePort
+//}
+//
+//func (i *Instance) Settings() *Settings {
+// return newSettings(i)
+//}
+//
+//func (i *Instance) settingParse(path string) (interface{}, error) {
+// var err error
+// var ok bool
+// var node map[string]interface{}
+// var data interface{}
+//
+// frames := strings.Split(path, ".")
+// node = i.release.Config
+//
+// if len(frames) > 1 {
+// for _, frame := range frames[0 : len(frames)-1] {
+// n, ok := node[frame]
+// if !ok {
+// err = ErrPathParseFailed
+// break
+// }
+// ntype := reflect.TypeOf(n)
+// if ntype.Kind() != reflect.Map {
+// err = ErrPathParseFailed
+// break
+// }
+// node = n.(map[string]interface{})
+// }
+// if err != nil {
+// return nil, err
+// }
+// data, ok = node[frames[len(frames)-1]]
+// } else {
+// data, ok = node[path]
+// }
+//
+// if !ok {
+// return nil, ErrPathParseFailed
+// }
+//
+// return data, nil
+//}
+//
+//func (i *Instance) GetComponents() []model.Component {
+// var components []model.Component
+//
+// components = append(components, model.Component{Name: i.ChartName})
+// deps, err := helm.ParseChartDependencies(i.release.Chart)
+// if err != nil {
+// return components
+// }
+//
+// for _, dep := range deps {
+// components = append(components, model.Component{
+// Name: dep.Name,
+// })
+// }
+//
+// return components
+//}
+//
+//func (i *Instance) GetSchemaCategories(component string) interface{} {
+// var data []string
+//
+// if component == i.ChartName {
+// data = helm.ParseChartCategories(i.release.Chart)
+// } else {
+// var exist = false
+// for _, dep := range i.release.Chart.Lock.Dependencies {
+// if dep.Name == component {
+// exist = true
+// break
+// }
+// }
+//
+// if exist {
+// ch, err := helm.GetChart(genChart("test", i.ChartName), i.CurrentChartVersion)
+// if err != nil {
+// return data
+// }
+//
+// for _, dep := range ch.Dependencies() {
+// if dep.Name() == component {
+// data = helm.ParseChartCategories(dep)
+// break
+// }
+// }
+// }
+// }
+//
+// return data
+//}
+//
+//func (i *Instance) GetSchema(component, category string) string {
+// var data string
+//
+// if component == i.ChartName {
+// jbody, err := helm.ReadSchemaFromChart(i.release.Chart, category, "test")
+// i.logger.Debugf("get schema content: %s", string(jbody))
+// if err != nil {
+// i.logger.WithError(err).Error("get schema failed")
+// return ""
+// }
+// data = string(jbody)
+// } else {
+// ch, err := helm.GetChart(genChart("test", i.ChartName), i.CurrentChartVersion)
+// if err != nil {
+// return ""
+// }
+// for _, dep := range ch.Dependencies() {
+// if dep.Name() == component {
+// jbody, err := helm.ReadSchemaFromChart(dep, category, "test")
+// if err != nil {
+// return ""
+// }
+// data = string(jbody)
+// }
+// }
+// }
+// return data
+//}
+//
+//func (i *Instance) GetPvcList() []model.AppRespPvc {
+// var result []model.AppRespPvc
+// pvcList, err := i.ks.Clients.Base.CoreV1().PersistentVolumeClaims(i.namespace).List(i.ctx, metav1.ListOptions{LabelSelector: i.selector.String()})
+// if err != nil {
+// i.logger.WithError(err).Error("list pvc failed")
+// return result
+// }
+// for _, pvc := range pvcList.Items {
+// quantity := pvc.Spec.Resources.Requests[v1.ResourceStorage]
+// p := model.AppRespPvc{
+// Name: pvc.Name, VolumeName: pvc.Spec.VolumeName, AccessModes: pvc.Spec.AccessModes,
+// Size: quantity.AsApproximateFloat64(),
+// Path: fmt.Sprintf("%s-%s-%s", pvc.Namespace, pvc.Name, pvc.Spec.VolumeName),
+// }
+// if pvc.Spec.StorageClassName != nil {
+// p.StorageClassName = *pvc.Spec.StorageClassName
+// }
+// result = append(result, p)
+// }
+// return result
+//}
+//
+//func (i *Instance) GetAccountInfo() map[string]string {
+// data := map[string]string{
+// "username": "", "password": "",
+// }
+//
+// values := i.release.Chart.Values
+// mergo.Merge(&values, i.release.Config, mergo.WithOverwriteWithEmptyValue)
+//
+// if auth, ok := values["auth"]; ok {
+// username := lookupFields(auth.(map[string]interface{}), "username", "user")
+// password := lookupFields(auth.(map[string]interface{}), "password", "passwd")
+// data["username"] = username
+// data["password"] = password
+// }
+//
+// return data
+//}
+//
+//func (i *Instance) Uninstall() error {
+// h, err := helm.NamespaceScope(i.namespace)
+// if err != nil {
+// return err
+// }
+//
+// installTime := i.release.Info.FirstDeployed.Time
+// uninstallTime := time.Now()
+//
+// dur := uninstallTime.Sub(installTime)
+//
+// err = h.Uninstall(i.name)
+// if err != nil {
+// analysis.UnInstall(i.ChartName, i.CurrentChartVersion).WithDuration(dur.Seconds()).Fail(err)
+// return err
+// }
+// analysis.UnInstall(i.ChartName, i.CurrentChartVersion).WithDuration(dur.Seconds()).Success()
+// return nil
+//}
+//
+//func (i *Instance) GetMetrics() *model.AppMetric {
+// metrics := i.ks.Metric.ListPodMetrics(i.ctx, i.namespace, i.selector)
+// pods, _ := i.ks.Store.ListPods(i.namespace, i.selector)
+//
+// var usage metric.Res
+// var limit metric.Res
+//
+// sumPodUsage(&usage, metrics)
+// sumPodLimit(&limit, pods)
+//
+// memUsage, _ := usage.Memory.AsInt64()
+// memLimit, _ := limit.Memory.AsInt64()
+//
+// data := model.AppMetric{
+// Cpu: model.ResourceCpu{
+// Usage: usage.Cpu.AsApproximateFloat64(), Limit: limit.Cpu.AsApproximateFloat64(),
+// },
+// Memory: model.ResourceMemory{
+// Usage: memUsage, Limit: memLimit,
+// },
+// }
+// return &data
+//}
+//
+//func sumPodUsage(dst *metric.Res, metrics []*metric.Res) {
+// dst.Cpu = resource.NewQuantity(0, resource.DecimalExponent)
+// dst.Memory = resource.NewQuantity(0, resource.DecimalExponent)
+//
+// for _, m := range metrics {
+// dst.Cpu.Add(m.Cpu.DeepCopy())
+// dst.Memory.Add(m.Memory.DeepCopy())
+// }
+//}
+//
+//func sumPodLimit(dst *metric.Res, pods []*v1.Pod) {
+// dst.Cpu = resource.NewQuantity(0, resource.DecimalExponent)
+// dst.Memory = resource.NewQuantity(0, resource.DecimalExponent)
+//
+// for _, pod := range pods {
+// for _, ctn := range pod.Spec.Containers {
+// l := ctn.Resources.Limits
+// dst.Cpu.Add(*l.Cpu())
+// dst.Memory.Add(*l.Memory())
+// }
+// }
+//}
+//
+//func lookupFields(m map[string]interface{}, names ...string) string {
+// for _, name := range names {
+// if v, ok := m[name]; ok {
+// refv := reflect.ValueOf(v)
+// if refv.Kind() == reflect.String {
+// return v.(string)
+// }
+// }
+// }
+// return ""
+//}
diff --git a/backend/internal/app/service/app/instance/account.go b/backend/internal/app/service/app/instance/account.go
new file mode 100644
index 00000000..444a68ec
--- /dev/null
+++ b/backend/internal/app/service/app/instance/account.go
@@ -0,0 +1,37 @@
+package instance
+
+import (
+ "reflect"
+
+ "github.com/imdario/mergo"
+)
+
+func (i *Instance) GetAccountInfo() map[string]string {
+ data := map[string]string{
+ "username": "", "password": "",
+ }
+
+ values := i.release.Chart.Values
+ mergo.Merge(&values, i.release.Config, mergo.WithOverwriteWithEmptyValue)
+
+ if auth, ok := values["auth"]; ok {
+ username := lookupFields(auth.(map[string]interface{}), "username", "user")
+ password := lookupFields(auth.(map[string]interface{}), "password", "passwd")
+ data["username"] = username
+ data["password"] = password
+ }
+
+ return data
+}
+
+func lookupFields(m map[string]interface{}, names ...string) string {
+ for _, name := range names {
+ if v, ok := m[name]; ok {
+ refv := reflect.ValueOf(v)
+ if refv.Kind() == reflect.String {
+ return v.(string)
+ }
+ }
+ }
+ return ""
+}
diff --git a/backend/internal/app/service/app/service_app_backup.go b/backend/internal/app/service/app/instance/backup.go
similarity index 79%
rename from backend/internal/app/service/app/service_app_backup.go
rename to backend/internal/app/service/app/instance/backup.go
index a40b4d8f..c5b70436 100644
--- a/backend/internal/app/service/app/service_app_backup.go
+++ b/backend/internal/app/service/app/instance/backup.go
@@ -1,14 +1,12 @@
-// Copyright (c) 2022 北京渠成软件有限公司 All rights reserved.
-// Use of this source code is governed by Z PUBLIC LICENSE 1.2 (ZPL 1.2)
-// license that can be found in the LICENSE file.
-
-package app
+package instance
import (
"fmt"
"strings"
"time"
+ "github.com/spf13/viper"
+
quchengv1beta1 "github.com/easysoft/quikon-api/qucheng/v1beta1"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -46,13 +44,17 @@ func (i *Instance) CreateBackup(username string) (interface{}, error) {
CreateTime int64 `json:"create_time"`
}{backupName, currTime.Unix()}
- _, err := i.ks.Clients.Cne.QuchengV1beta1().Backups(constant.DefaultRuntimeNamespace).Create(i.ctx, &backupReq, metav1.CreateOptions{})
+ ns := viper.GetString(constant.FlagRuntimeNamespace)
+
+ _, err := i.Ks.Clients.Cne.QuchengV1beta1().Backups(ns).Create(i.Ctx, &backupReq, metav1.CreateOptions{})
return data, err
}
func (i *Instance) GetBackupList() ([]model.AppRespBackup, error) {
var result []model.AppRespBackup
- backups, err := i.ks.Store.ListBackups(constant.DefaultRuntimeNamespace, i.selector)
+
+ ns := viper.GetString(constant.FlagRuntimeNamespace)
+ backups, err := i.Ks.Store.ListBackups(ns, i.selector)
if err != nil {
return nil, err
}
@@ -70,12 +72,12 @@ func (i *Instance) GetBackupList() ([]model.AppRespBackup, error) {
bkReq, _ := labels.NewRequirement(constant.LabelBackupName, "==", []string{b.Name})
bkLabel := i.selector.Add(*bkReq)
- dbBackups, _ := i.ks.Store.ListDbBackups(constant.DefaultRuntimeNamespace, labels.NewSelector().Add(*bkReq))
- volumeBackups, _ := i.ks.Store.ListVolumeBackups(constant.DefaultRuntimeNamespace, labels.NewSelector().Add(*bkReq))
+ dbBackups, _ := i.Ks.Store.ListDbBackups(ns, labels.NewSelector().Add(*bkReq))
+ volumeBackups, _ := i.Ks.Store.ListVolumeBackups(ns, labels.NewSelector().Add(*bkReq))
item.BackupDetails = i.statisticBackupDetail(dbBackups, volumeBackups)
- restores, err := i.ks.Store.ListRestores(constant.DefaultRuntimeNamespace, bkLabel)
+ restores, err := i.Ks.Store.ListRestores(ns, bkLabel)
if err != nil {
return result, err
}
@@ -114,14 +116,14 @@ func (i *Instance) statisticBackupDetail(dbs []*quchengv1beta1.DbBackup, volumes
info.Size, _ = db.Status.Size.AsInt64()
}
- _db, err := i.ks.Store.GetDb(db.Spec.Db.Namespace, db.Spec.Db.Name)
+ _db, err := i.Ks.Store.GetDb(db.Spec.Db.Namespace, db.Spec.Db.Name)
if err == nil {
info.DbName = _db.Spec.DbName
targetNs := _db.Namespace
if _db.Spec.TargetService.Namespace != "" {
targetNs = _db.Spec.TargetService.Namespace
}
- dbsvc, err := i.ks.Store.GetDbService(targetNs, _db.Spec.TargetService.Name)
+ dbsvc, err := i.Ks.Store.GetDbService(targetNs, _db.Spec.TargetService.Name)
if err == nil {
info.DbType = string(dbsvc.Spec.Type)
}
@@ -131,7 +133,7 @@ func (i *Instance) statisticBackupDetail(dbs []*quchengv1beta1.DbBackup, volumes
for _, vol := range volumes {
info := model.AppPvcBackupInfo{
- PvcName: vol.Labels[constant.LableVeleroPvcUID],
+ PvcName: vol.Labels[constant.LabelVeleroPvcUID],
Volume: vol.Spec.Volume,
Status: strings.ToLower(string(vol.Status.Phase)),
}
@@ -149,7 +151,8 @@ func (i *Instance) statisticBackupDetail(dbs []*quchengv1beta1.DbBackup, volumes
}
func (i *Instance) GetBackupStatus(backupName string) (interface{}, error) {
- backup, err := i.ks.Store.GetBackup(constant.DefaultRuntimeNamespace, backupName)
+ ns := viper.GetString(constant.FlagRuntimeNamespace)
+ backup, err := i.Ks.Store.GetBackup(ns, backupName)
if err != nil {
return nil, err
}
@@ -188,12 +191,14 @@ func (i *Instance) CreateRestore(backupName string, username string) (interface{
CreateTime int64 `json:"create_time"`
}{backupName, currTime.Unix()}
- _, err := i.ks.Clients.Cne.QuchengV1beta1().Restores(constant.DefaultRuntimeNamespace).Create(i.ctx, &restoreReq, metav1.CreateOptions{})
+ ns := viper.GetString(constant.FlagRuntimeNamespace)
+ _, err := i.Ks.Clients.Cne.QuchengV1beta1().Restores(ns).Create(i.Ctx, &restoreReq, metav1.CreateOptions{})
return data, err
}
func (i *Instance) GetRestoreStatus(restoreName string) (interface{}, error) {
- restore, err := i.ks.Store.GetRestore(constant.DefaultRuntimeNamespace, restoreName)
+ ns := viper.GetString(constant.FlagRuntimeNamespace)
+ restore, err := i.Ks.Store.GetRestore(ns, restoreName)
if err != nil {
return nil, err
}
@@ -205,3 +210,11 @@ func (i *Instance) GetRestoreStatus(restoreName string) (interface{}, error) {
return data, nil
}
+
+func (i *Instance) GetDbList() []*quchengv1beta1.Db {
+ l, err := i.Ks.Store.ListDb(i.namespace, i.selector)
+ if err != nil {
+ i.logger.WithError(err).Error("find dbs failed")
+ }
+ return l
+}
diff --git a/backend/internal/app/service/app/instance/deprecated.go b/backend/internal/app/service/app/instance/deprecated.go
new file mode 100644
index 00000000..6ae85bcf
--- /dev/null
+++ b/backend/internal/app/service/app/instance/deprecated.go
@@ -0,0 +1,85 @@
+package instance
+
+import (
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
+)
+
+func (i *Instance) GetComponents() []model.Component {
+ var components []model.Component
+
+ components = append(components, model.Component{Name: i.ChartName})
+ deps, err := helm.ParseChartDependencies(i.release.Chart)
+ if err != nil {
+ return components
+ }
+
+ for _, dep := range deps {
+ components = append(components, model.Component{
+ Name: dep.Name,
+ })
+ }
+
+ return components
+}
+
+func (i *Instance) GetSchemaCategories(component string) interface{} {
+ var data []string
+
+ if component == i.ChartName {
+ data = helm.ParseChartCategories(i.release.Chart)
+ } else {
+ var exist = false
+ for _, dep := range i.release.Chart.Lock.Dependencies {
+ if dep.Name == component {
+ exist = true
+ break
+ }
+ }
+
+ if exist {
+ ch, err := helm.GetChart(helm.GenChart("test", i.ChartName), i.CurrentChartVersion)
+ if err != nil {
+ return data
+ }
+
+ for _, dep := range ch.Dependencies() {
+ if dep.Name() == component {
+ data = helm.ParseChartCategories(dep)
+ break
+ }
+ }
+ }
+ }
+
+ return data
+}
+
+func (i *Instance) GetSchema(component, category string) string {
+ var data string
+
+ if component == i.ChartName {
+ jbody, err := helm.ReadSchemaFromChart(i.release.Chart, category, "test")
+ i.logger.Debugf("get schema content: %s", string(jbody))
+ if err != nil {
+ i.logger.WithError(err).Error("get schema failed")
+ return ""
+ }
+ data = string(jbody)
+ } else {
+ ch, err := helm.GetChart(helm.GenChart("test", i.ChartName), i.CurrentChartVersion)
+ if err != nil {
+ return ""
+ }
+ for _, dep := range ch.Dependencies() {
+ if dep.Name() == component {
+ jbody, err := helm.ReadSchemaFromChart(dep, category, "test")
+ if err != nil {
+ return ""
+ }
+ data = string(jbody)
+ }
+ }
+ }
+ return data
+}
diff --git a/backend/internal/app/service/app/instance/error.go b/backend/internal/app/service/app/instance/error.go
new file mode 100644
index 00000000..355352ef
--- /dev/null
+++ b/backend/internal/app/service/app/instance/error.go
@@ -0,0 +1,9 @@
+package instance
+
+import "errors"
+
+var (
+ ErrAppNotFound = errors.New("release not found")
+
+ ErrPathParseFailed = errors.New("release path parse failed")
+)
diff --git a/backend/internal/app/service/app/instance/extra.go b/backend/internal/app/service/app/instance/extra.go
new file mode 100644
index 00000000..10298a6a
--- /dev/null
+++ b/backend/internal/app/service/app/instance/extra.go
@@ -0,0 +1,32 @@
+package instance
+
+import (
+ "fmt"
+
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+)
+
+func (i *Instance) GetPvcList() []model.AppRespPvc {
+ var result []model.AppRespPvc
+ pvcList, err := i.Ks.Clients.Base.CoreV1().PersistentVolumeClaims(i.namespace).List(i.Ctx, metav1.ListOptions{LabelSelector: i.selector.String()})
+ if err != nil {
+ i.logger.WithError(err).Error("list pvc failed")
+ return result
+ }
+ for _, pvc := range pvcList.Items {
+ quantity := pvc.Spec.Resources.Requests[v1.ResourceStorage]
+ p := model.AppRespPvc{
+ Name: pvc.Name, VolumeName: pvc.Spec.VolumeName, AccessModes: pvc.Spec.AccessModes,
+ Size: quantity.AsApproximateFloat64(),
+ Path: fmt.Sprintf("%s-%s-%s", pvc.Namespace, pvc.Name, pvc.Spec.VolumeName),
+ }
+ if pvc.Spec.StorageClassName != nil {
+ p.StorageClassName = *pvc.Spec.StorageClassName
+ }
+ result = append(result, p)
+ }
+ return result
+}
diff --git a/backend/internal/app/service/app/helm.go b/backend/internal/app/service/app/instance/helm.go
similarity index 99%
rename from backend/internal/app/service/app/helm.go
rename to backend/internal/app/service/app/instance/helm.go
index 2208af91..8d448bae 100644
--- a/backend/internal/app/service/app/helm.go
+++ b/backend/internal/app/service/app/instance/helm.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by Z PUBLIC LICENSE 1.2 (ZPL 1.2)
// license that can be found in the LICENSE file.
-package app
+package instance
import (
"bytes"
diff --git a/backend/internal/app/service/app/instance/instance.go b/backend/internal/app/service/app/instance/instance.go
new file mode 100644
index 00000000..48680ac8
--- /dev/null
+++ b/backend/internal/app/service/app/instance/instance.go
@@ -0,0 +1,91 @@
+package instance
+
+import (
+ "context"
+
+ "github.com/sirupsen/logrus"
+ "helm.sh/helm/v3/pkg/release"
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/labels"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
+)
+
+type Instance struct {
+ Ctx context.Context
+
+ clusterName string
+ namespace string
+ name string
+
+ selector labels.Selector
+
+ Ks *cluster.Cluster
+
+ release *release.Release
+ secret *v1.Secret
+
+ ChartName string
+ CurrentChartVersion string
+
+ logger logrus.FieldLogger
+}
+
+func NewInstance(ctx context.Context, name string, clusterName, namespace string, ks *cluster.Cluster) (*Instance, error) {
+
+ i := &Instance{
+ Ctx: ctx,
+ clusterName: clusterName, namespace: namespace, name: name,
+ Ks: ks,
+ logger: logging.DefaultLogger().WithContext(ctx).WithFields(logrus.Fields{
+ "name": name, "namespace": namespace,
+ }),
+ }
+
+ i.release = i.fetchRelease()
+ if i.release == nil {
+ return nil, ErrAppNotFound
+ }
+ err := i.prepare()
+ return i, err
+}
+
+func (i *Instance) prepare() error {
+ i.ChartName = i.release.Chart.Metadata.Name
+ i.CurrentChartVersion = i.release.Chart.Metadata.Version
+
+ i.selector = labels.Set{"release": i.name}.AsSelector()
+ secret, err := loadAppSecret(i.Ctx, i.name, i.namespace, i.release.Version, i.Ks)
+ if err != nil {
+ i.logger.WithError(err).Errorf("got release secret failed with resivion %d", i.release.Version)
+ return err
+ }
+ i.secret = secret
+ return nil
+}
+
+func (i *Instance) fetchRelease() *release.Release {
+ getter := &ReleaseGetter{
+ namespace: i.namespace,
+ store: i.Ks.Store,
+ }
+ rel, err := getter.Last(i.name)
+ if err != nil {
+ i.logger.WithError(err).Error("parse release failed")
+ }
+ return rel
+}
+
+func (i *Instance) isApp() bool {
+ v, ok := i.secret.Labels[constant.LabelApplication]
+ if ok && v == "true" {
+ return true
+ }
+ return false
+}
+
+func (i *Instance) GetLogger() logrus.FieldLogger {
+ return i.logger
+}
diff --git a/backend/internal/app/service/app/instance/metric.go b/backend/internal/app/service/app/instance/metric.go
new file mode 100644
index 00000000..f3e860f7
--- /dev/null
+++ b/backend/internal/app/service/app/instance/metric.go
@@ -0,0 +1,56 @@
+package instance
+
+import (
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/metric"
+)
+
+func (i *Instance) GetMetrics() *model.AppMetric {
+ metrics := i.Ks.Metric.ListPodMetrics(i.Ctx, i.namespace, i.selector)
+ pods, _ := i.Ks.Store.ListPods(i.namespace, i.selector)
+
+ var usage metric.Res
+ var limit metric.Res
+
+ sumPodUsage(&usage, metrics)
+ sumPodLimit(&limit, pods)
+
+ memUsage, _ := usage.Memory.AsInt64()
+ memLimit, _ := limit.Memory.AsInt64()
+
+ data := model.AppMetric{
+ Cpu: model.ResourceCpu{
+ Usage: usage.Cpu.AsApproximateFloat64(), Limit: limit.Cpu.AsApproximateFloat64(),
+ },
+ Memory: model.ResourceMemory{
+ Usage: memUsage, Limit: memLimit,
+ },
+ }
+ return &data
+}
+
+func sumPodUsage(dst *metric.Res, metrics []*metric.Res) {
+ dst.Cpu = resource.NewQuantity(0, resource.DecimalExponent)
+ dst.Memory = resource.NewQuantity(0, resource.DecimalExponent)
+
+ for _, m := range metrics {
+ dst.Cpu.Add(m.Cpu.DeepCopy())
+ dst.Memory.Add(m.Memory.DeepCopy())
+ }
+}
+
+func sumPodLimit(dst *metric.Res, pods []*v1.Pod) {
+ dst.Cpu = resource.NewQuantity(0, resource.DecimalExponent)
+ dst.Memory = resource.NewQuantity(0, resource.DecimalExponent)
+
+ for _, pod := range pods {
+ for _, ctn := range pod.Spec.Containers {
+ l := ctn.Resources.Limits
+ dst.Cpu.Add(*l.Cpu())
+ dst.Memory.Add(*l.Memory())
+ }
+ }
+}
diff --git a/backend/internal/app/service/app/setting.go b/backend/internal/app/service/app/instance/setting.go
similarity index 75%
rename from backend/internal/app/service/app/setting.go
rename to backend/internal/app/service/app/instance/setting.go
index ccd79347..f1cdd3fe 100644
--- a/backend/internal/app/service/app/setting.go
+++ b/backend/internal/app/service/app/instance/setting.go
@@ -1,10 +1,9 @@
-// Copyright (c) 2022 北京渠成软件有限公司 All rights reserved.
-// Use of this source code is governed by Z PUBLIC LICENSE 1.2 (ZPL 1.2)
-// license that can be found in the LICENSE file.
-
-package app
+package instance
import (
+ "reflect"
+ "strings"
+
"github.com/imdario/mergo"
"gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
@@ -131,3 +130,45 @@ type stringSetting struct {
Key string `json:"key"`
Val string `json:"value"`
}
+
+func (i *Instance) Settings() *Settings {
+ return newSettings(i)
+}
+
+func (i *Instance) settingParse(path string) (interface{}, error) {
+ var err error
+ var ok bool
+ var node map[string]interface{}
+ var data interface{}
+
+ frames := strings.Split(path, ".")
+ node = i.release.Config
+
+ if len(frames) > 1 {
+ for _, frame := range frames[0 : len(frames)-1] {
+ n, ok := node[frame]
+ if !ok {
+ err = ErrPathParseFailed
+ break
+ }
+ ntype := reflect.TypeOf(n)
+ if ntype.Kind() != reflect.Map {
+ err = ErrPathParseFailed
+ break
+ }
+ node = n.(map[string]interface{})
+ }
+ if err != nil {
+ return nil, err
+ }
+ data, ok = node[frames[len(frames)-1]]
+ } else {
+ data, ok = node[path]
+ }
+
+ if !ok {
+ return nil, ErrPathParseFailed
+ }
+
+ return data, nil
+}
diff --git a/backend/internal/app/service/app/instance/status.go b/backend/internal/app/service/app/instance/status.go
new file mode 100644
index 00000000..af3dd8f6
--- /dev/null
+++ b/backend/internal/app/service/app/instance/status.go
@@ -0,0 +1,121 @@
+package instance
+
+import (
+ v1 "k8s.io/api/core/v1"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app/component"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
+)
+
+func (i *Instance) getServices() ([]*v1.Service, error) {
+ return i.Ks.Store.ListServices(i.namespace, i.selector)
+}
+
+func (i *Instance) getComponents() *component.Components {
+ components := component.NewComponents()
+
+ deployments, _ := i.Ks.Store.ListDeployments(i.namespace, i.selector)
+
+ if len(deployments) >= 1 {
+ for _, d := range deployments {
+ components.Add(component.NewDeployComponent(d, i.Ks))
+ }
+ }
+
+ statefulsets, _ := i.Ks.Store.ListStatefulSets(i.namespace, i.selector)
+
+ if len(statefulsets) >= 1 {
+ for _, s := range statefulsets {
+ components.Add(component.NewStatefulsetComponent(s, i.Ks))
+ }
+ }
+
+ return components
+}
+
+func (i *Instance) ParseStatus() *model.AppRespStatus {
+ var settingStopped = false
+ components := i.getComponents()
+
+ data := &model.AppRespStatus{
+ Components: make([]model.AppRespStatusComponent, 0),
+ Status: constant.AppStatusMap[constant.AppStatusUnknown],
+ Version: i.CurrentChartVersion,
+ Age: 0,
+ }
+
+ if len(components.Items()) == 0 {
+ return data
+ }
+
+ stopped, err := i.settingParse("global.stopped")
+ if err == nil {
+ if stop, ok := stopped.(bool); ok {
+ settingStopped = stop
+ }
+ }
+
+ for _, c := range components.Items() {
+ resC := model.AppRespStatusComponent{
+ Name: c.Name(),
+ Kind: c.Kind(),
+ Replicas: c.Replicas(),
+ StatusCode: c.Status(settingStopped),
+ Status: constant.AppStatusMap[c.Status(settingStopped)],
+ Age: c.Age(),
+ }
+ data.Components = append(data.Components, resC)
+ }
+
+ minStatusCode := data.Components[0].StatusCode
+ maxAge := data.Components[0].Age
+ for _, comp := range data.Components {
+ if comp.StatusCode < minStatusCode {
+ minStatusCode = comp.StatusCode
+ }
+
+ if comp.Age > maxAge {
+ maxAge = comp.Age
+ }
+ }
+
+ data.Status = constant.AppStatusMap[minStatusCode]
+ data.Age = maxAge
+ return data
+}
+
+func (i *Instance) ListIngressHosts() []string {
+ var hosts []string
+ ingresses, err := i.Ks.Store.ListIngresses(i.namespace, i.selector)
+ if err != nil {
+ return hosts
+ }
+ for _, ing := range ingresses {
+ for _, rule := range ing.Spec.Rules {
+ hosts = append(hosts, rule.Host)
+ }
+ }
+ return hosts
+}
+
+func (i *Instance) ParseNodePort() int32 {
+ var nodePort int32 = 0
+ services, err := i.getServices()
+ if err != nil {
+ return nodePort
+ }
+
+ for _, s := range services {
+ if s.Spec.Type == v1.ServiceTypeNodePort {
+ for _, p := range s.Spec.Ports {
+ if p.Name == constant.ServicePortWeb || p.Name == "http" {
+ nodePort = p.NodePort
+ break
+ }
+ }
+ }
+ }
+
+ return nodePort
+}
diff --git a/backend/internal/app/service/app/upgrade.go b/backend/internal/app/service/app/instance/update.go
similarity index 66%
rename from backend/internal/app/service/app/upgrade.go
rename to backend/internal/app/service/app/instance/update.go
index 009e1f93..be42fa28 100644
--- a/backend/internal/app/service/app/upgrade.go
+++ b/backend/internal/app/service/app/instance/update.go
@@ -1,11 +1,10 @@
-// Copyright (c) 2022 北京渠成软件有限公司 All rights reserved.
-// Use of this source code is governed by Z PUBLIC LICENSE 1.2 (ZPL 1.2)
-// license that can be found in the LICENSE file.
-
-package app
+package instance
import (
"os"
+ "time"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/analysis"
"helm.sh/helm/v3/pkg/release"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,34 +34,13 @@ func (i *Instance) Stop(chart, channel string) error {
}
h, _ := helm.NamespaceScope(i.namespace)
- if rel, err := h.Upgrade(i.name, genChart(channel, chart), i.CurrentChartVersion, options); err != nil {
+ if rel, err := h.Upgrade(i.name, helm.GenChart(channel, chart), i.CurrentChartVersion, options); err != nil {
return err
} else {
return i.updateSecretMeta(rel)
}
}
-func (i *Instance) updateSecretMeta(rel *release.Release) error {
- if !i.isApp() {
- return nil
- }
- secretMeta := metav1.ObjectMeta{
- Labels: map[string]string{
- constant.LabelApplication: "true",
- },
- Annotations: make(map[string]string),
- }
- if creator, ok := i.secret.Annotations[constant.AnnotationAppCreator]; ok {
- secretMeta.Annotations[constant.AnnotationAppCreator] = creator
- }
- if channel, ok := i.secret.Annotations[constant.AnnotationAppChannel]; ok {
- secretMeta.Annotations[constant.AnnotationAppChannel] = channel
- }
-
- err := completeAppLabels(i.ctx, rel, i.ks, i.logger, secretMeta)
- return err
-}
-
func (i *Instance) Start(chart, channel string) error {
h, _ := helm.NamespaceScope(i.namespace)
vals := i.release.Config
@@ -82,7 +60,7 @@ func (i *Instance) Start(chart, channel string) error {
if err = helm.RepoUpdate(); err != nil {
return err
}
- if rel, err := h.Upgrade(i.name, genChart(channel, chart), i.CurrentChartVersion, options); err != nil {
+ if rel, err := h.Upgrade(i.name, helm.GenChart(channel, chart), i.CurrentChartVersion, options); err != nil {
return err
} else {
// add easyfost label for last secret
@@ -90,7 +68,7 @@ func (i *Instance) Start(chart, channel string) error {
}
}
-func (i *Instance) PatchSettings(chart string, body model.AppCreateOrUpdateModel) error {
+func (i *Instance) PatchSettings(chart string, body model.AppCreateOrUpdateModel, snippetSettings map[string]interface{}) error {
var (
err error
vals map[string]interface{}
@@ -119,14 +97,25 @@ func (i *Instance) PatchSettings(chart string, body model.AppCreateOrUpdateModel
ValueFiles: []string{lastValFile},
}
+ if len(snippetSettings) > 0 {
+ snippetValueFile, err := writeValuesFile(snippetSettings)
+ if err != nil {
+ i.logger.WithError(err).Error("write values file failed")
+ } else {
+ defer os.Remove(snippetValueFile)
+ options.ValueFiles = append(options.ValueFiles, snippetValueFile)
+ }
+ }
+
if len(body.SettingsMap) > 0 {
i.logger.Infof("load patch settings map: %+v", body.SettingsMap)
f, err := writeValuesFile(body.SettingsMap)
if err != nil {
i.logger.WithError(err).Error("write values file failed")
+ } else {
+ defer os.Remove(f)
+ options.ValueFiles = append(options.ValueFiles, f)
}
- defer os.Remove(f)
- options.ValueFiles = append(options.ValueFiles, f)
}
if err = h.PatchValues(vals, settings); err != nil {
@@ -136,26 +125,58 @@ func (i *Instance) PatchSettings(chart string, body model.AppCreateOrUpdateModel
if body.Version != "" {
version = body.Version
}
+
if version != i.CurrentChartVersion {
if err = helm.RepoUpdate(); err != nil {
return err
}
}
- if rel, err := h.Upgrade(i.name, genChart(body.Channel, chart), version, options); err != nil {
+ if rel, err := h.Upgrade(i.name, helm.GenChart(body.Channel, chart), version, options); err != nil {
+ analysis.Upgrade(body.Chart, version).Fail(err)
return err
} else {
+ analysis.Upgrade(body.Chart, version).Success()
return i.updateSecretMeta(rel)
}
}
-func genRepo(channel string) string {
- c := "test"
- if channel != "" {
- c = channel
+func (i *Instance) Uninstall() error {
+ h, err := helm.NamespaceScope(i.namespace)
+ if err != nil {
+ return err
+ }
+
+ installTime := i.release.Info.FirstDeployed.Time
+ uninstallTime := time.Now()
+
+ dur := uninstallTime.Sub(installTime)
+
+ err = h.Uninstall(i.name)
+ if err != nil {
+ analysis.UnInstall(i.ChartName, i.CurrentChartVersion).WithDuration(dur.Seconds()).Fail(err)
+ return err
}
- return "qucheng-" + c
+ analysis.UnInstall(i.ChartName, i.CurrentChartVersion).WithDuration(dur.Seconds()).Success()
+ return nil
}
-func genChart(channel, chart string) string {
- return genRepo(channel) + "/" + chart
+func (i *Instance) updateSecretMeta(rel *release.Release) error {
+ if !i.isApp() {
+ return nil
+ }
+ secretMeta := metav1.ObjectMeta{
+ Labels: map[string]string{
+ constant.LabelApplication: "true",
+ },
+ Annotations: make(map[string]string),
+ }
+ if creator, ok := i.secret.Annotations[constant.AnnotationAppCreator]; ok {
+ secretMeta.Annotations[constant.AnnotationAppCreator] = creator
+ }
+ if channel, ok := i.secret.Annotations[constant.AnnotationAppChannel]; ok {
+ secretMeta.Annotations[constant.AnnotationAppChannel] = channel
+ }
+
+ err := completeAppLabels(i.Ctx, rel, i.Ks, i.logger, secretMeta)
+ return err
}
diff --git a/backend/internal/app/service/app/instance/util.go b/backend/internal/app/service/app/instance/util.go
new file mode 100644
index 00000000..81478685
--- /dev/null
+++ b/backend/internal/app/service/app/instance/util.go
@@ -0,0 +1,81 @@
+package instance
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "strconv"
+
+ "github.com/sirupsen/logrus"
+ "gopkg.in/yaml.v3"
+ "helm.sh/helm/v3/pkg/release"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/types"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
+)
+
+func loadAppSecret(ctx context.Context, name, namespace string, revision int, ks *cluster.Cluster) (*v1.Secret, error) {
+ var targetSecret *v1.Secret
+
+ selector := labels.Set{"name": name, "owner": "helm", "version": strconv.Itoa(revision)}.AsSelector()
+ secrets, err := ks.Store.ListSecrets(namespace, selector)
+ if err != nil {
+ return nil, err
+ }
+
+ count := len(secrets)
+ if count == 1 {
+ targetSecret = secrets[0]
+ }
+
+ if targetSecret == nil {
+ secretList, err := ks.Clients.Base.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
+ if err != nil {
+ return nil, err
+ }
+ count = len(secretList.Items)
+ if count != 1 {
+ e := fmt.Errorf("get release secret failed, expect 1 got %d", count)
+ return nil, e
+ }
+ targetSecret = &secretList.Items[0]
+ }
+
+ return targetSecret, nil
+}
+
+func writeValuesFile(data map[string]interface{}) (string, error) {
+ f, err := ioutil.TempFile("/tmp", "values.******.yaml")
+ if err != nil {
+ return "", err
+ }
+ vars, err := yaml.Marshal(data)
+ if err != nil {
+ return "nil", err
+ }
+ _, err = f.Write(vars)
+ if err != nil {
+ return "nil", err
+ }
+ _ = f.Close()
+ return f.Name(), nil
+}
+
+func completeAppLabels(ctx context.Context, rel *release.Release, ks *cluster.Cluster, logger logrus.FieldLogger, meta metav1.ObjectMeta) error {
+ logger.Info("start complete app labels")
+ latestSecret, err := loadAppSecret(ctx, rel.Name, rel.Namespace, rel.Version, ks)
+ if err != nil {
+ return err
+ }
+
+ t := struct {
+ Metadata metav1.ObjectMeta `json:"metadata"`
+ }{meta}
+ patchContent, _ := json.Marshal(&t)
+ _, err = ks.Clients.Base.CoreV1().Secrets(latestSecret.Namespace).Patch(ctx, latestSecret.Name, types.MergePatchType, patchContent, metav1.PatchOptions{})
+ return err
+}
diff --git a/backend/internal/app/service/app/manage.go b/backend/internal/app/service/app/manage.go
index f4098784..66cab6bb 100644
--- a/backend/internal/app/service/app/manage.go
+++ b/backend/internal/app/service/app/manage.go
@@ -5,22 +5,24 @@
package app
import (
+ "bytes"
+ "compress/gzip"
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
+ "io/ioutil"
"github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/releaseutil"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app/instance"
+
"gitlab.zcorp.cc/pangu/cne-api/internal/pkg/constant"
"gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
- "io/ioutil"
- "os"
"strconv"
- "gopkg.in/yaml.v3"
- "helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,7 +31,6 @@ import (
"gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
"gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
- "gitlab.zcorp.cc/pangu/cne-api/pkg/helm"
)
type Manager struct {
@@ -51,77 +52,8 @@ func NewApps(ctx context.Context, clusterName, namespace string) *Manager {
}
}
-func (m *Manager) Install(name string, body model.AppCreateOrUpdateModel) error {
- logger := m.logger.WithFields(logrus.Fields{
- "name": name, "namespace": body.Namespace,
- })
- h, err := helm.NamespaceScope(m.namespace)
- if err != nil {
- return err
- }
-
- var settings = make([]string, len(body.Settings))
- for _, s := range body.Settings {
- settings = append(settings, s.Key+"="+s.Val)
- }
- options := &values.Options{Values: settings}
- logger.Infof("user custom settings is %+v", settings)
-
- if len(body.SettingsMap) > 0 {
- logger.Infof("user custom settingsMap is %+v", body.SettingsMap)
- f, err := writeValuesFile(body.SettingsMap)
- if err != nil {
- logger.WithError(err).Error("write values file failed")
- }
- defer os.Remove(f)
- options.ValueFiles = []string{f}
- }
-
- if err = helm.RepoUpdate(); err != nil {
- logger.WithError(err).Error("helm update repo failed")
- return err
- }
-
- rel, err := h.Install(name, genChart(body.Channel, body.Chart), body.Version, options)
- if err != nil {
- logger.WithError(err).Error("helm install failed")
- if _, e := h.GetRelease(name); e == nil {
- logger.Info("recycle incomplete release")
- _ = h.Uninstall(name)
- }
- }
- secretMeta := metav1.ObjectMeta{
- Labels: map[string]string{
- constant.LabelApplication: "true",
- },
- Annotations: map[string]string{
- constant.AnnotationAppChannel: body.Channel,
- },
- }
- if body.Username != "" {
- secretMeta.Annotations[constant.AnnotationAppCreator] = body.Username
- }
- err = completeAppLabels(m.ctx, rel, m.ks, logger, secretMeta)
- return err
-}
-
-func (m *Manager) UnInstall(name string) error {
- h, err := helm.NamespaceScope(m.namespace)
- if err != nil {
- return err
- }
-
- err = h.Uninstall(name)
- return err
-}
-
-func (m *Manager) GetApp(name string) (*Instance, error) {
- app := newApp(m.ctx, m, name)
- if app.release == nil {
- return nil, ErrAppNotFound
- }
-
- err := app.prepare()
+func (m *Manager) GetApp(name string) (*instance.Instance, error) {
+ app, err := instance.NewInstance(m.ctx, name, m.clusterName, m.namespace, m.ks)
return app, err
}
@@ -198,23 +130,6 @@ func (m *Manager) ListAllApplications() (interface{}, error) {
return result, nil
}
-func writeValuesFile(data map[string]interface{}) (string, error) {
- f, err := ioutil.TempFile("/tmp", "values.******.yaml")
- if err != nil {
- return "", err
- }
- vars, err := yaml.Marshal(data)
- if err != nil {
- return "nil", err
- }
- _, err = f.Write(vars)
- if err != nil {
- return "nil", err
- }
- _ = f.Close()
- return f.Name(), nil
-}
-
func completeAppLabels(ctx context.Context, rel *release.Release, ks *cluster.Cluster, logger logrus.FieldLogger, meta metav1.ObjectMeta) error {
logger.Info("start complete app labels")
latestSecret, err := loadAppSecret(ctx, rel.Name, rel.Namespace, rel.Version, ks)
@@ -259,3 +174,45 @@ func loadAppSecret(ctx context.Context, name, namespace string, revision int, ks
return targetSecret, nil
}
+
+/*
+helm func
+*/
+
+var b64 = base64.StdEncoding
+
+var magicGzip = []byte{0x1f, 0x8b, 0x08}
+
+// decodeRelease decodes the bytes of data into a release
+// type. Data must contain a base64 encoded gzipped string of a
+// valid release, otherwise an error is returned.
+func decodeRelease(data string) (*release.Release, error) {
+ // base64 decode string
+ b, err := b64.DecodeString(data)
+ if err != nil {
+ return nil, err
+ }
+
+ // For backwards compatibility with releases that were stored before
+ // compression was introduced we skip decompression if the
+ // gzip magic header is not found
+ if bytes.Equal(b[0:3], magicGzip) {
+ r, err := gzip.NewReader(bytes.NewReader(b))
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ b2, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+ b = b2
+ }
+
+ var rls release.Release
+ // unmarshal release object bytes
+ if err := json.Unmarshal(b, &rls); err != nil {
+ return nil, err
+ }
+ return &rls, nil
+}
diff --git a/backend/internal/app/service/component/component.go b/backend/internal/app/service/component/component.go
index 7c1a6929..7a71bd32 100644
--- a/backend/internal/app/service/component/component.go
+++ b/backend/internal/app/service/component/component.go
@@ -52,6 +52,7 @@ func (m *Manager) ListDbService(namespace string, onlyGlobal string) ([]model.Co
Release: dbSvc.Labels["release"],
DbType: string(dbSvc.Spec.Type),
Alias: decodeDbSvcAlias(dbSvc),
+ Ready: dbSvc.Status.Ready,
}
components = append(components, s)
}
@@ -68,6 +69,7 @@ func (m *Manager) ListDbService(namespace string, onlyGlobal string) ([]model.Co
Release: dbSvc.Labels["release"],
DbType: string(dbSvc.Spec.Type),
Alias: decodeDbSvcAlias(dbSvc),
+ Ready: dbSvc.Status.Ready,
}
components = append(components, s)
}
@@ -117,7 +119,7 @@ func (m *Manager) ListGlobalDbsComponents(kind, namespace string) ([]model.Compo
selector := labels.SelectorFromSet(map[string]string{
constant.LabelGlobalDatabase: "true",
})
- dbsvcs, err := m.ks.Store.ListDbService(namespace, selector)
+ dbsvcs, err := m.ks.Store.ListDbService("", selector)
if err != nil {
return nil, err
}
diff --git a/backend/internal/app/service/namespace/instance.go b/backend/internal/app/service/namespace/instance.go
index 68bdc4d6..1eca19bf 100644
--- a/backend/internal/app/service/namespace/instance.go
+++ b/backend/internal/app/service/namespace/instance.go
@@ -4,14 +4,32 @@
package namespace
-import v1 "k8s.io/api/core/v1"
+import (
+ "context"
+
+ "github.com/sirupsen/logrus"
+ v1 "k8s.io/api/core/v1"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
+)
type Instance struct {
- ns *v1.Namespace
+ ctx context.Context
+ namespace string
+ ns *v1.Namespace
+ ks *cluster.Cluster
+ logger logrus.FieldLogger
}
-func newInstance(ns *v1.Namespace) *Instance {
- return &Instance{ns: ns}
+func newInstance(ctx context.Context, ns *v1.Namespace, ks *cluster.Cluster) *Instance {
+ return &Instance{
+ ctx: ctx,
+ namespace: ns.Name,
+ ns: ns,
+ ks: ks,
+ logger: logging.DefaultLogger().WithContext(ctx).WithField("namespace", ns),
+ }
}
func (i *Instance) WriteAble() bool {
diff --git a/backend/internal/app/service/namespace/manage.go b/backend/internal/app/service/namespace/manage.go
index 341cdc1a..60b75a02 100644
--- a/backend/internal/app/service/namespace/manage.go
+++ b/backend/internal/app/service/namespace/manage.go
@@ -59,5 +59,5 @@ func (m *Manager) Has(name string) bool {
func (m *Manager) Get(name string) *Instance {
ns, _ := m.ks.Clients.Base.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
- return newInstance(ns)
+ return newInstance(m.ctx, ns, m.ks)
}
diff --git a/backend/internal/app/service/service.go b/backend/internal/app/service/service.go
index 4ddbd542..a2858e31 100644
--- a/backend/internal/app/service/service.go
+++ b/backend/internal/app/service/service.go
@@ -7,6 +7,8 @@ package service
import (
"context"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/service/snippet"
+
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service/app"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service/component"
"gitlab.zcorp.cc/pangu/cne-api/internal/app/service/middleware"
@@ -33,3 +35,7 @@ func Middlewares(ctx context.Context) *middleware.Manager {
func Components(ctx context.Context, clusterName string) *component.Manager {
return component.NewComponents(ctx, "primary")
}
+
+func Snippets(ctx context.Context, clusterName string) *snippet.Manager {
+ return snippet.NewSnippets(ctx, "primary")
+}
diff --git a/backend/internal/app/service/snippet/const.go b/backend/internal/app/service/snippet/const.go
new file mode 100644
index 00000000..3fdb09b2
--- /dev/null
+++ b/backend/internal/app/service/snippet/const.go
@@ -0,0 +1,10 @@
+package snippet
+
+const (
+ NamePrefix = "snippet-"
+ snippetContentKey = "content"
+
+ LabelSnippetConfig = "easycorp.io/snippet-config"
+ annotationSnippetConfigCategory = "easycorp.io/snippet-config/category"
+ annotationSnippetConfigAutoImport = "easycorp.io/snippet-config/auto-import"
+)
diff --git a/backend/internal/app/service/snippet/manage.go b/backend/internal/app/service/snippet/manage.go
new file mode 100644
index 00000000..2c798ad0
--- /dev/null
+++ b/backend/internal/app/service/snippet/manage.go
@@ -0,0 +1,99 @@
+package snippet
+
+import (
+ "context"
+
+ "github.com/sirupsen/logrus"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/app/model"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/cluster"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
+)
+
+type Manager struct {
+ ctx context.Context
+
+ clusterName string
+ ks *cluster.Cluster
+ logger logrus.FieldLogger
+}
+
+func NewSnippets(ctx context.Context, clusterName string) *Manager {
+ return &Manager{
+ ctx: ctx,
+ clusterName: clusterName,
+ ks: cluster.Get(clusterName),
+ logger: logging.DefaultLogger().WithContext(ctx),
+ }
+}
+
+func (m *Manager) List(namespace string) ([]*model.SnippetConfig, error) {
+ var data []*model.SnippetConfig
+ cmList, err := m.ks.Store.ListConfigMaps(namespace, labels.Set{LabelSnippetConfig: "true"}.AsSelector())
+ if err != nil {
+ return data, err
+ }
+
+ for _, cm := range cmList {
+ snippet, err := NewSnippet(cm)
+ if err != nil {
+ m.logger.WithError(err).Error("convert configmap to snippet failed")
+ continue
+ }
+ sc := &model.SnippetConfig{
+ Name: cm.Name,
+ Namespace: cm.Namespace,
+ Category: snippet.Category(),
+ Content: snippet.content,
+ AutoImport: snippet.IsAutoImport(),
+ }
+ data = append(data, sc)
+ }
+
+ return data, nil
+}
+
+func (m *Manager) Get(name, namespace string) (*Snippet, error) {
+ cm, err := m.ks.Store.GetConfigMap(namespace, name)
+ if err != nil {
+ return nil, err
+ }
+
+ s, err := NewSnippet(cm)
+ return s, err
+}
+
+func (m *Manager) Create(name, namespace, content string) error {
+
+ cm := v1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name, Namespace: namespace,
+ Labels: labels.Set{
+ LabelSnippetConfig: "true",
+ },
+ },
+ Data: map[string]string{
+ snippetContentKey: content,
+ },
+ }
+ _, err := m.ks.Clients.Base.CoreV1().ConfigMaps(namespace).Create(m.ctx, &cm, metav1.CreateOptions{})
+ return err
+}
+
+func (m *Manager) Update(name, namespace, content string) error {
+ cm, err := m.ks.Store.GetConfigMap(namespace, name)
+ if err != nil {
+ return err
+ }
+
+ cm.Data[snippetContentKey] = content
+ _, err = m.ks.Clients.Base.CoreV1().ConfigMaps(namespace).Update(m.ctx, cm, metav1.UpdateOptions{})
+ return err
+}
+
+func (m *Manager) Remove(name, namespace string) error {
+ return m.ks.Clients.Base.CoreV1().ConfigMaps(namespace).Delete(m.ctx, name, metav1.DeleteOptions{})
+}
diff --git a/backend/internal/app/service/snippet/snippet.go b/backend/internal/app/service/snippet/snippet.go
new file mode 100644
index 00000000..0f452769
--- /dev/null
+++ b/backend/internal/app/service/snippet/snippet.go
@@ -0,0 +1,55 @@
+package snippet
+
+import (
+ "fmt"
+
+ "gopkg.in/yaml.v3"
+ v1 "k8s.io/api/core/v1"
+)
+
+type Snippet struct {
+ cm *v1.ConfigMap
+ content string
+ values map[string]interface{}
+}
+
+func NewSnippet(cm *v1.ConfigMap) (*Snippet, error) {
+ s := &Snippet{
+ cm: cm,
+ }
+ content, ok := cm.Data[snippetContentKey]
+ if !ok {
+ return nil, fmt.Errorf("content key '%s' not found", snippetContentKey)
+ }
+ s.content = content
+ err := yaml.Unmarshal([]byte(content), &s.values)
+ if err != nil {
+ return nil, err
+ }
+ return s, nil
+}
+
+func (s *Snippet) Category() string {
+ return s.cm.Annotations[annotationSnippetConfigCategory]
+}
+
+func (s *Snippet) IsAutoImport() bool {
+ v, ok := s.cm.Annotations[annotationSnippetConfigAutoImport]
+ if !ok {
+ return false
+ }
+
+ if v == "true" {
+ return true
+ }
+
+ return false
+}
+
+func (s *Snippet) Content() string {
+ return s.content
+}
+
+func (s *Snippet) Values() map[string]interface{} {
+ return s.values
+}
diff --git a/backend/internal/pkg/analysis/action.go b/backend/internal/pkg/analysis/action.go
new file mode 100644
index 00000000..80b1cb44
--- /dev/null
+++ b/backend/internal/pkg/analysis/action.go
@@ -0,0 +1,87 @@
+package analysis
+
+import "encoding/json"
+
+type model struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Action string `json:"action"`
+ Successful bool `json:"success"`
+ Err string `json:"err"`
+
+ Extra string `json:"extra_properties"`
+ extraSource map[string]interface{} `json:"-"`
+}
+
+func newModel(name, version, action string) model {
+ return model{
+ Name: name,
+ Version: version,
+ Action: action,
+ extraSource: make(map[string]interface{}),
+ }
+}
+
+func (m *model) renderExtra() {
+ if len(m.extraSource) > 0 {
+ content, err := json.Marshal(m.extraSource)
+ if err == nil {
+ m.Extra = string(content)
+ }
+ }
+}
+
+func (m *model) Success() {
+ m.Successful = true
+ m.renderExtra()
+ content, _ := json.Marshal(m)
+ _analysis.write(string(content))
+}
+
+func (m *model) Fail(err error) {
+ m.Successful = false
+ m.Err = err.Error()
+ m.renderExtra()
+ content, _ := json.Marshal(m)
+ _analysis.write(string(content))
+}
+
+type modelInstall struct {
+ model
+}
+
+func Install(name, version string) *modelInstall {
+ return &modelInstall{
+ newModel(name, version, "install"),
+ }
+}
+
+func (m *modelInstall) WithFeatures(feats []string) *model {
+ m.model.extraSource["features"] = feats
+ return &m.model
+}
+
+type modelUninstall struct {
+ model
+}
+
+func UnInstall(name, version string) *modelUninstall {
+ return &modelUninstall{
+ newModel(name, version, "uninstall"),
+ }
+}
+
+func (m *modelUninstall) WithDuration(dur float64) *model {
+ m.model.extraSource["duration"] = dur
+ return &m.model
+}
+
+type modelUpgrade struct {
+ model
+}
+
+func Upgrade(name, version string) *modelUpgrade {
+ return &modelUpgrade{
+ newModel(name, version, "upgrade"),
+ }
+}
diff --git a/backend/internal/pkg/analysis/analysis.go b/backend/internal/pkg/analysis/analysis.go
new file mode 100644
index 00000000..7a39a033
--- /dev/null
+++ b/backend/internal/pkg/analysis/analysis.go
@@ -0,0 +1,87 @@
+package analysis
+
+import (
+ "context"
+ "time"
+
+ "github.com/sirupsen/logrus"
+
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/httplib/market"
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/logging"
+)
+
+type Analysis struct {
+ queue []string
+ maxQueueLength int
+ bufferChan chan string
+ flushInterval int64
+ client *market.Client
+
+ logger *logrus.Logger
+ disabled bool
+}
+
+var _analysis *Analysis
+
+func Init() *Analysis {
+ logger := logging.DefaultLogger()
+ _analysis = &Analysis{
+ queue: make([]string, 0),
+ maxQueueLength: 1024,
+ bufferChan: make(chan string),
+ flushInterval: 1,
+ client: market.New(),
+ logger: logger,
+ disabled: false,
+ }
+ return _analysis
+}
+
+func (s *Analysis) write(data string) {
+ if !s.disabled {
+ logging.DefaultLogger().Infof("content %s", data)
+ s.bufferChan <- data
+ }
+}
+
+func (s *Analysis) Run(ctx context.Context) {
+ s.process(ctx)
+}
+
+func (s *Analysis) process(ctx context.Context) {
+ ticker := time.NewTicker(time.Duration(s.flushInterval) * time.Second)
+loop:
+ for {
+ select {
+ case l := <-s.bufferChan:
+ s.queue = append(s.queue, l)
+ if len(s.queue) >= s.maxQueueLength {
+ s.flush()
+ }
+ case <-ticker.C:
+ s.flush()
+ case <-ctx.Done():
+ ticker.Stop()
+ s.flush()
+ break loop
+ }
+ }
+}
+
+func (s *Analysis) flush() {
+ var err error
+ length := len(s.queue)
+ if length == 0 {
+ return
+ }
+
+ s.logger.Infof("flush analysis data, %d records will be upload", length)
+ for _, item := range s.queue {
+ body := item
+ err = s.client.SendAppAnalysis(body)
+ if err != nil {
+ s.logger.WithError(err).Error("request market api server failed")
+ }
+ }
+ s.queue = s.queue[:0]
+}
diff --git a/backend/internal/pkg/constant/app.go b/backend/internal/pkg/constant/app.go
index c5de8718..94707ba2 100644
--- a/backend/internal/pkg/constant/app.go
+++ b/backend/internal/pkg/constant/app.go
@@ -56,7 +56,6 @@ const (
)
const (
- LableVeleroPvcUID = "velero.io/pvc-uid"
+ LabelVeleroPvcUID = "velero.io/pvc-uid"
)
-
-const DefaultRuntimeNamespace = "cne-system"
+const FlagRuntimeNamespace = "pod-namespace"
diff --git a/backend/internal/pkg/db/manage/manage.go b/backend/internal/pkg/db/manage/manage.go
new file mode 100644
index 00000000..6702c611
--- /dev/null
+++ b/backend/internal/pkg/db/manage/manage.go
@@ -0,0 +1,32 @@
+package manage
+
+import (
+ quchengv1beta1 "github.com/easysoft/quikon-api/qucheng/v1beta1"
+)
+
+type DbManager interface {
+ DbType() quchengv1beta1.DbType
+ ServerInfo() DbServerInfo
+ IsValid(meta *DbMeta) error
+}
+
+type DbServerInfo interface {
+ Host() string
+ Port() int32
+ Address() string
+}
+
+type DbServiceMeta struct {
+ Type quchengv1beta1.DbType
+ Host string
+ Port int32
+ AdminUser string
+ AdminPassword string
+}
+
+type DbMeta struct {
+ Name string
+ User string
+ Password string
+ Config map[string]string
+}
diff --git a/backend/internal/pkg/db/manage/mysql.go b/backend/internal/pkg/db/manage/mysql.go
new file mode 100644
index 00000000..e3126beb
--- /dev/null
+++ b/backend/internal/pkg/db/manage/mysql.go
@@ -0,0 +1,54 @@
+package manage
+
+import (
+ "database/sql"
+ "fmt"
+
+ quchengv1beta1 "github.com/easysoft/quikon-api/qucheng/v1beta1"
+ _ "github.com/go-sql-driver/mysql"
+)
+
+const (
+ _mysqlDriver = "mysql"
+)
+
+type mysqlManage struct {
+ meta DbServiceMeta
+}
+
+func newMysqlManager(meta DbServiceMeta) (DbManager, error) {
+ return &mysqlManage{
+ meta: meta,
+ }, nil
+}
+
+func (m *mysqlManage) DbType() quchengv1beta1.DbType {
+ return m.meta.Type
+}
+
+func (m *mysqlManage) ServerInfo() DbServerInfo {
+ return &serverInfo{
+ host: m.meta.Host, port: m.meta.Port,
+ }
+}
+
+func (m *mysqlManage) IsValid(meta *DbMeta) error {
+ dsn := m.genBusinessDsn(meta)
+ dbClient, err := sql.Open(_mysqlDriver, dsn)
+ if err != nil {
+ return err
+ }
+ defer dbClient.Close()
+
+ return dbClient.Ping()
+}
+
+func (m *mysqlManage) genAdminDsn() string {
+ return fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8mb4&parseTime=True&loc=Local",
+ m.meta.AdminUser, m.meta.AdminPassword, m.meta.Host, m.meta.Port)
+}
+
+func (m *mysqlManage) genBusinessDsn(dbMeta *DbMeta) string {
+ return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+ dbMeta.User, dbMeta.Password, m.meta.Host, m.meta.Port, dbMeta.Name)
+}
diff --git a/backend/internal/pkg/db/manage/parse.go b/backend/internal/pkg/db/manage/parse.go
new file mode 100644
index 00000000..9eef517c
--- /dev/null
+++ b/backend/internal/pkg/db/manage/parse.go
@@ -0,0 +1,123 @@
+package manage
+
+import (
+ "context"
+ "fmt"
+
+ quchengv1beta1 "github.com/easysoft/quikon-api/qucheng/v1beta1"
+ "github.com/pkg/errors"
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube"
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/store"
+)
+
+func ParseDB(ctx context.Context, c *store.Storer, db *quchengv1beta1.Db) (DbManager, *DbMeta, error) {
+ var err error
+ var dbSvc *quchengv1beta1.DbService
+ var dbMgr DbManager
+ var dbMeta *DbMeta
+
+ target := db.Spec.TargetService
+ ns := target.Namespace
+ if ns == "" {
+ ns = db.Namespace
+ }
+
+ dbSvc, err = c.GetDbService(ns, target.Name)
+ if err != nil {
+ return nil, nil, err
+ }
+ dbMgr, err = ParseDbService(ctx, c, dbSvc)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ dbUser, dbPass, err := readAccount(ctx, c, db.Namespace, &db.Spec.Account)
+ if err != nil {
+ return nil, nil, err
+ }
+ dbMeta = &DbMeta{
+ Name: db.Spec.DbName,
+ User: dbUser,
+ Password: dbPass,
+ }
+
+ return dbMgr, dbMeta, nil
+}
+
+func ParseDbService(ctx context.Context, c *store.Storer, dbSvc *quchengv1beta1.DbService) (DbManager, error) {
+ svc := dbSvc.Spec.Service
+ host, port, err := readService(ctx, c, dbSvc.Namespace, &svc)
+ if err != nil {
+ return nil, err
+ }
+
+ dbUser, dbPass, err := readAccount(ctx, c, dbSvc.Namespace, &dbSvc.Spec.Account)
+ if err != nil {
+ return nil, err
+ }
+
+ meta := DbServiceMeta{
+ Type: dbSvc.Spec.Type,
+ Host: host,
+ Port: port,
+ AdminUser: dbUser,
+ AdminPassword: dbPass,
+ }
+
+ switch meta.Type {
+ case quchengv1beta1.DbTypeMysql:
+ return newMysqlManager(meta)
+ default:
+ return nil, errors.New("dbType is not supported")
+ }
+}
+
+func readService(ctx context.Context, c *store.Storer, namespace string, svc *quchengv1beta1.Service) (string, int32, error) {
+ targetNs := svc.Namespace
+ if targetNs == "" {
+ targetNs = namespace
+ }
+ host := fmt.Sprintf("%s.%s.svc", svc.Name, targetNs)
+
+ var (
+ port int32
+ err error
+ s *v1.Service
+ )
+
+ if svc.Port.Type == intstr.Int {
+ port = svc.Port.IntVal
+ } else {
+ s, err = c.GetService(targetNs, svc.Name)
+ if err != nil {
+ return "", 0, err
+ }
+ for _, p := range s.Spec.Ports {
+ if p.Name == svc.Port.StrVal {
+ port = p.Port
+ break
+ }
+ }
+ }
+
+ if port == 0 {
+ return "", 0, fmt.Errorf("parse service port '%s' failed", svc.Port.StrVal)
+ }
+
+ return host, port, nil
+}
+
+func readAccount(ctx context.Context, c *store.Storer, namespace string, info *quchengv1beta1.Account) (string, string, error) {
+ user, err := kube.ReadValueSource(c, namespace, kube.NewValueRef(info.User.Value, info.User.ValueFrom))
+ if err != nil {
+ return "", "", err
+ }
+ passwd, err := kube.ReadValueSource(c, namespace, kube.NewValueRef(info.Password.Value, info.Password.ValueFrom))
+ if err != nil {
+ return "", "", err
+ }
+ return user, passwd, nil
+}
diff --git a/backend/internal/pkg/db/manage/server.go b/backend/internal/pkg/db/manage/server.go
new file mode 100644
index 00000000..0e20bb17
--- /dev/null
+++ b/backend/internal/pkg/db/manage/server.go
@@ -0,0 +1,20 @@
+package manage
+
+import "fmt"
+
+type serverInfo struct {
+ host string
+ port int32
+}
+
+func (s *serverInfo) Host() string {
+ return s.host
+}
+
+func (s *serverInfo) Port() int32 {
+ return s.port
+}
+
+func (s *serverInfo) Address() string {
+ return fmt.Sprintf("%s:%d", s.host, s.port)
+}
diff --git a/backend/internal/pkg/kube/store/store.go b/backend/internal/pkg/kube/store/store.go
index 305c942b..06b1634d 100644
--- a/backend/internal/pkg/kube/store/store.go
+++ b/backend/internal/pkg/kube/store/store.go
@@ -46,6 +46,7 @@ type Informer struct {
Services cache.SharedIndexInformer
Endpoints cache.SharedIndexInformer
Secrets cache.SharedIndexInformer
+ ConfigMaps cache.SharedIndexInformer
Deployments cache.SharedIndexInformer
StatefulSets cache.SharedIndexInformer
@@ -65,6 +66,7 @@ func (i *Informer) Run(stopCh chan struct{}) {
go i.Ingresses.Run(stopCh)
go i.Services.Run(stopCh)
go i.Endpoints.Run(stopCh)
+ go i.ConfigMaps.Run(stopCh)
go i.Secrets.Run(stopCh)
go i.Deployments.Run(stopCh)
go i.StatefulSets.Run(stopCh)
@@ -82,6 +84,7 @@ func (i *Informer) Run(stopCh chan struct{}) {
i.Ingresses.HasSynced,
i.Services.HasSynced,
i.Endpoints.HasSynced,
+ i.ConfigMaps.HasSynced,
i.Secrets.HasSynced,
i.Deployments.HasSynced,
i.StatefulSets.HasSynced,
@@ -103,6 +106,7 @@ type Lister struct {
Ingresses networkv1.IngressLister
Services v1.ServiceLister
Endpoints v1.EndpointsLister
+ ConfigMaps v1.ConfigMapLister
Secrets v1.SecretLister
Deployments appsv1.DeploymentLister
StatefulSets appsv1.StatefulSetLister
@@ -160,6 +164,9 @@ func NewStorer(config rest.Config) *Storer {
s.informers.Endpoints = factory.Core().V1().Endpoints().Informer()
s.listers.Endpoints = factory.Core().V1().Endpoints().Lister()
+ s.informers.ConfigMaps = factory.Core().V1().ConfigMaps().Informer()
+ s.listers.ConfigMaps = factory.Core().V1().ConfigMaps().Lister()
+
s.informers.Secrets = factory.Core().V1().Secrets().Informer()
s.listers.Secrets = factory.Core().V1().Secrets().Lister()
@@ -258,6 +265,14 @@ func (s *Storer) ListEndpoints(namespace string, selector labels.Selector) ([]*m
return s.listers.Endpoints.Endpoints(namespace).List(selector)
}
+func (s *Storer) GetConfigMap(namespace, name string) (*metav1.ConfigMap, error) {
+ return s.listers.ConfigMaps.ConfigMaps(namespace).Get(name)
+}
+
+func (s *Storer) ListConfigMaps(namespace string, selector labels.Selector) ([]*metav1.ConfigMap, error) {
+ return s.listers.ConfigMaps.ConfigMaps(namespace).List(selector)
+}
+
func (s *Storer) GetSecret(namespace, name string) (*metav1.Secret, error) {
return s.listers.Secrets.Secrets(namespace).Get(name)
}
diff --git a/backend/internal/pkg/kube/value_read.go b/backend/internal/pkg/kube/value_read.go
new file mode 100644
index 00000000..baf041eb
--- /dev/null
+++ b/backend/internal/pkg/kube/value_read.go
@@ -0,0 +1,73 @@
+// Copyright (c) 2022-2022 北京渠成软件有限公司(Beijing Qucheng Software Co., Ltd. www.qucheng.com) All rights reserved.
+// Use of this source code is covered by the following dual licenses:
+// (1) Z PUBLIC LICENSE 1.2 (ZPL 1.2)
+// (2) Affero General Public License 3.0 (AGPL 3.0)
+// license that can be found in the LICENSE file.
+
+package kube
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "gitlab.zcorp.cc/pangu/cne-api/internal/pkg/kube/store"
+
+ "github.com/easysoft/quikon-api/qucheng/v1beta1"
+ v1 "k8s.io/api/core/v1"
+)
+
+type valueRefer interface {
+ Default() string
+ Source() *v1beta1.ValueSource
+}
+
+type valueRef struct {
+ value string
+ source *v1beta1.ValueSource
+}
+
+func NewValueRef(value string, source *v1beta1.ValueSource) valueRefer {
+ return &valueRef{
+ value: value,
+ source: source,
+ }
+}
+
+func (v *valueRef) Default() string {
+ return v.value
+}
+
+func (v *valueRef) Source() *v1beta1.ValueSource {
+ return v.source
+}
+
+func ReadValueSource(c *store.Storer, namespace string, ref valueRefer) (string, error) {
+ ctx := context.TODO()
+ source := ref.Source()
+ if source != nil {
+ if source.SecretKeyRef != nil {
+ return getSecretRefValue(ctx, c, namespace, source.SecretKeyRef)
+ }
+ }
+ if ref.Default() != "" {
+ return ref.Default(), nil
+ } else {
+ return "", errors.New("no resource ref defined")
+ }
+}
+
+// getSecretRefValue returns the value of a secret in the supplied namespace
+func getSecretRefValue(ctx context.Context, c *store.Storer, namespace string, secretSelector *v1.SecretKeySelector) (string, error) {
+ var err error
+ var secret *v1.Secret
+ secret, err = c.GetSecret(namespace, secretSelector.Name)
+ if err != nil {
+ return "", err
+ }
+ if data, ok := secret.Data[secretSelector.Key]; ok {
+ return string(data), nil
+ }
+ return "", fmt.Errorf("key %s not found in secret %s", secretSelector.Key, secretSelector.Name)
+
+}
diff --git a/backend/pkg/cron/cron.go b/backend/pkg/cron/cron.go
index 0e8889d2..feb7d003 100644
--- a/backend/pkg/cron/cron.go
+++ b/backend/pkg/cron/cron.go
@@ -38,6 +38,6 @@ func (c *Client) Stop() {
c.client.Stop()
}
-func init() {
- Cron = New()
-}
+//func init() {
+// Cron = New()
+//}
diff --git a/backend/pkg/httplib/market/market_client.go b/backend/pkg/httplib/market/market_client.go
new file mode 100644
index 00000000..ba8ccbaf
--- /dev/null
+++ b/backend/pkg/httplib/market/market_client.go
@@ -0,0 +1,45 @@
+package market
+
+import (
+ "github.com/kelseyhightower/envconfig"
+ "github.com/parnurzeal/gorequest"
+ "github.com/sirupsen/logrus"
+
+ "gitlab.zcorp.cc/pangu/cne-api/pkg/httplib"
+)
+
+// Client for gallery with session cache
+type Client struct {
+ *httplib.HTTPServer
+
+ client *gorequest.SuperAgent
+ Token string
+}
+
+// New Client
+func New() *Client {
+ server := httplib.HTTPServer{}
+ _ = envconfig.Process("CNE_MARKET_API", &server)
+ if server.Host == "" || server.Port == "" {
+ panic("environment CNE_MARKET_API_HOST and CNE_MARKET_API_PORT must be set")
+ }
+
+ c := &Client{
+ HTTPServer: &server,
+ client: gorequest.New().SetDebug(server.Debug),
+ }
+ return c
+}
+
+func (c *Client) SendAppAnalysis(body string) error {
+
+ uri := httplib.GenerateURL(c.HTTPServer, "/api/market/analysis/put")
+
+ resp, body, errs := c.client.Post(uri).SendString(body).End()
+ if len(errs) != 0 {
+ return errs[0]
+ }
+ logrus.Debug(resp.StatusCode, resp.Body)
+
+ return nil
+}
diff --git a/backend/pkg/httplib/type.go b/backend/pkg/httplib/type.go
new file mode 100644
index 00000000..8c04ec73
--- /dev/null
+++ b/backend/pkg/httplib/type.go
@@ -0,0 +1,11 @@
+package httplib
+
+// HTTPServer provide a common definition
+type HTTPServer struct {
+ Schema string `envconfig:"SCHEMA" default:"http"`
+ Host string `envconfig:"HOST" default:"127.0.0.1"`
+ Port string `envconfig:"PORT" default:"8088"`
+ UserName string
+ PassWord string
+ Debug bool `envconfig:"DEBUG" default:"false"`
+}
diff --git a/backend/pkg/httplib/util.go b/backend/pkg/httplib/util.go
new file mode 100644
index 00000000..a9b02bbf
--- /dev/null
+++ b/backend/pkg/httplib/util.go
@@ -0,0 +1,39 @@
+package httplib
+
+import (
+ "bytes"
+ "net/url"
+)
+
+// GenerateHTTPServer return the full http host
+func GenerateHTTPServer(server *HTTPServer) string {
+ var httpserver bytes.Buffer
+ httpserver.WriteString(server.Schema)
+ httpserver.WriteString("://")
+ httpserver.WriteString(server.Host)
+ if (server.Schema == "http" && server.Port != "80") || (server.Schema == "https" && server.Port != "443") {
+ httpserver.WriteString(":" + server.Port)
+ }
+ return httpserver.String()
+}
+
+// GenerateURL return the full http request url
+// with query paramters encode
+func GenerateURL(server *HTTPServer, path string, queries ...[2]string) string {
+ var uri bytes.Buffer
+ uri.WriteString(GenerateHTTPServer(server))
+ if path != "" {
+ uri.WriteString(path)
+ }
+
+ if len(queries) > 0 {
+ uri.WriteString("?")
+ v := url.Values{}
+ for _, item := range queries {
+ v.Add(item[0], item[1])
+ }
+ uri.WriteString(v.Encode())
+ }
+
+ return uri.String()
+}
diff --git a/backend/pkg/logging/logging.go b/backend/pkg/logging/logging.go
index 8e97eb29..0265bf04 100644
--- a/backend/pkg/logging/logging.go
+++ b/backend/pkg/logging/logging.go
@@ -8,18 +8,21 @@ import (
"strings"
"time"
+ "github.com/spf13/viper"
+
"github.com/sirupsen/logrus"
)
+const FlagLogLevel = "log-level"
+
var (
defaultLogger *logrus.Logger
)
-func init() {
- defaultLogger = NewLogger()
-}
-
func DefaultLogger() *logrus.Logger {
+ if defaultLogger == nil {
+ defaultLogger = NewLogger()
+ }
return defaultLogger
}
@@ -51,6 +54,16 @@ func NewLogger() *logrus.Logger {
return "", fmt.Sprintf("%s:%d", filename, f.Line)
},
}
+
+ lv := viper.GetString(FlagLogLevel)
+ level, err := logrus.ParseLevel(lv)
+ if err != nil {
+ logger.WithError(err).Fatalf("setup log level '%s' failed", lv)
+ } else {
+ logger.SetLevel(level)
+ logger.Infof("setup log level to %s", lv)
+ }
+
logger.AddHook(&ContextFieldsHook{})
return logger
}
@@ -70,5 +83,3 @@ func readModuleName() string {
return moduleName
}
-
-var _ = DefaultLogger()
diff --git a/backend/pkg/utils/kvpath/parse.go b/backend/pkg/utils/kvpath/parse.go
new file mode 100644
index 00000000..f1332670
--- /dev/null
+++ b/backend/pkg/utils/kvpath/parse.go
@@ -0,0 +1,84 @@
+package kvpath
+
+import (
+ "reflect"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+var ErrPathParseFailed = errors.New("release path parse failed")
+
+func ReadBool(node map[string]interface{}, path string) bool {
+ var v bool
+ data, err := read(node, path)
+ if err != nil {
+ return false
+ }
+
+ v, ok := data.(bool)
+ if !ok {
+ return false
+ }
+
+ return v
+}
+
+func ReadString(node map[string]interface{}, path string) string {
+ var v string
+ data, err := read(node, path)
+ if err != nil {
+ return ""
+ }
+
+ v, ok := data.(string)
+ if !ok {
+ return ""
+ }
+
+ return v
+}
+
+func Exist(node map[string]interface{}, path string) bool {
+ _, err := read(node, path)
+ if err != nil && err == ErrPathParseFailed {
+ return false
+ }
+ return true
+}
+
+func read(node map[string]interface{}, path string) (interface{}, error) {
+ var err error
+ var ok bool
+ var data interface{}
+
+ frames := strings.Split(path, ".")
+
+ if len(frames) > 1 {
+ for _, frame := range frames[0 : len(frames)-1] {
+ n, ok := node[frame]
+ if !ok {
+ err = ErrPathParseFailed
+ break
+ }
+ ntype := reflect.TypeOf(n)
+ if ntype.Kind() != reflect.Map {
+ err = ErrPathParseFailed
+ break
+ }
+ node = n.(map[string]interface{})
+ }
+ if err != nil {
+ return nil, err
+ }
+ data, ok = node[frames[len(frames)-1]]
+ } else {
+ data, ok = node[path]
+ }
+
+ if !ok {
+ return nil, ErrPathParseFailed
+ }
+
+ return data, nil
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index c0ab18f1..e7b1c2bb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,6 +33,10 @@ services:
- CLOUD_DEFAULT_CHANNEL=test
- DEBUG=1
- ENABLE_BACKEND=true
+ - CNE_MARKET_API_SCHEMA=http
+ - CNE_MARKET_API_HOST=cne-market.internal-pre.chandao.net
+ - CNE_MARKET_API_PORT=80
+ - ALLOW_SELECT_VERSION=true
qucheng-dev:
image: hub.qucheng.com/platform/qucheng:${TAG}
@@ -62,6 +66,10 @@ services:
- CLOUD_DEFAULT_CHANNEL=test
- APP_DOMAIN=qc.yunop.com
- CLOUD_SWITCH_CHANNEL=true
+ - CNE_MARKET_API_SCHEMA=https
+ - CNE_MARKET_API_HOST=api.qucheng.com
+ - CNE_MARKET_API_PORT=443
+ - ALLOW_SELECT_VERSION=true
volumes:
db:
diff --git a/docker/rootfs/etc/s6/s6-init/envs b/docker/rootfs/etc/s6/s6-init/envs
index 5c21fe92..a4308c12 100644
--- a/docker/rootfs/etc/s6/s6-init/envs
+++ b/docker/rootfs/etc/s6/s6-init/envs
@@ -19,6 +19,7 @@ export MYSQL_DB=${MYSQL_DB:-qucheng}
export MYSQL_USER=${MYSQL_USER:-root}
export MYSQL_PASSWORD=${MYSQL_PASSWORD:-pass4Qucheng}
export MAXWAIT=${MAXWAIT:-30}
+export ALLOW_SELECT_VERSION=${ALLOW_SELECT_VERSION:-false}
#==========#
# Apache #
@@ -40,4 +41,4 @@ export PERSISTENCE_LIST="
# Persistence directory and make soft link #
#==========================================#
export PERSISTENCE_LINK_LIST="
-/data/qucheng/www/data"
\ No newline at end of file
+/data/qucheng/www/data"
diff --git a/frontend/config/config.php b/frontend/config/config.php
index 135c2ad1..4d53630e 100644
--- a/frontend/config/config.php
+++ b/frontend/config/config.php
@@ -17,7 +17,7 @@
/* 基本设置。Basic settings. */
$config->platformVersion = getenv('CHART_VERSION');// 渠成平台版本。The version of Qucheng platform.
-$config->version = '1.2.0'; // 渠成Web版本。 The version of Web Service of Qucheng. Don't change it.
+$config->version = '1.4.0'; // 渠成Web版本。 The version of Web Service of Qucheng. Don't change it.
$config->charset = 'UTF-8'; // 编码。 The encoding of Qucheng.
$config->cookieLife = time() + 2592000; // Cookie的生存时间。The cookie life time.
$config->timezone = 'Asia/Shanghai'; // 时区设置。 The time zone setting, for more see http://www.php.net/manual/en/timezones.php.
diff --git a/frontend/config/qucheng.php b/frontend/config/qucheng.php
index c96ba618..8b11c815 100644
--- a/frontend/config/qucheng.php
+++ b/frontend/config/qucheng.php
@@ -97,6 +97,8 @@
define('TABLE_ENTRY', '`' . $config->db->prefix . 'entry`');
+define('TABLE_NAVINSTANCE', '`' . $config->db->prefix . 'navinstance`');
+
if(!defined('TABLE_LANG')) define('TABLE_LANG', '`' . $config->db->prefix . 'lang`');
$config->objectTables['instance'] = TABLE_INSTANCE;
diff --git a/frontend/db/data.sql b/frontend/db/data.sql
index 45cb161b..ddf02c09 100644
--- a/frontend/db/data.sql
+++ b/frontend/db/data.sql
@@ -203,5 +203,18 @@ CREATE TABLE `q_grouppriv` (
UNIQUE KEY `group` (`group`,`module`,`method`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3;
-REPLACE INTO `q_company` ( `name`, `admins`) VALUES ('', '');
+DROP TABLE IF EXISTS `q_navinstance`;
+CREATE TABLE IF NOT EXISTS `q_navinstance` (
+ `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
+ `title` char(50),
+ `domain` char(255) NOT NULL,
+ `logo` varchar(255),
+ `desc` text,
+ `pinned` enum('0', '1') NOT NULL DEFAULT '0',
+ `createdBy` char(30) NOT NULL,
+ `createdAt` datetime NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+REPLACE INTO `q_company` (`name`, `admins`) VALUES ('', '');
REPLACE INTO `q_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'common', 'global', 'version', '1.1.1');
diff --git a/frontend/db/update1.4.0.sql b/frontend/db/update1.4.0.sql
new file mode 100644
index 00000000..ef51acfd
--- /dev/null
+++ b/frontend/db/update1.4.0.sql
@@ -0,0 +1,14 @@
+DROP TABLE IF EXISTS `q_navinstance`;
+CREATE TABLE IF NOT EXISTS `q_navinstance` (
+ `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
+ `title` char(50),
+ `domain` char(255) NOT NULL,
+ `logo` varchar(255),
+ `desc` text,
+ `pinned` enum('0', '1') NOT NULL DEFAULT '0',
+ `createdBy` char(30) NOT NULL,
+ `createdAt` datetime NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+REPLACE INTO `q_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'navigation', 'global', 'hideInaccessible', 'off');
diff --git a/frontend/db/update1.4.1.sql b/frontend/db/update1.4.1.sql
new file mode 100644
index 00000000..9d0ee83b
--- /dev/null
+++ b/frontend/db/update1.4.1.sql
@@ -0,0 +1,3 @@
+set sql_mode = '';
+-- REPLACE INTO `q_user` (`account`, `realname`, `nickname`, `password`, `company`, `commiter`) VALUES ('demo', 'demo', 'demo', md5('demo'), 0, '');
+REPLACE INTO `q_user` (`company`, `type`, `dept`, `account`, `password`, `role`, `realname`, `pinyin`, `nickname`, `commiter`, `avatar`, `birthday`, `gender`, `email`, `skype`, `qq`, `mobile`, `phone`, `weixin`, `dingding`, `slack`, `whatsapp`, `address`, `zipcode`, `nature`, `analysis`, `strategy`, `join`, `visits`, `visions`, `ip`, `last`, `fails`, `locked`, `feedback`, `ranzhi`, `ldap`, `score`, `scoreLevel`, `deleted`, `clientStatus`, `clientLang`) VALUES (0,'inside',0,'demo',md5('demo'),'','demo','','','','','1970-01-01','f','','','','','','','','','','','','','','','1970-01-01',406,'','10.10.16.8',1660699163,0,'0000-00-00 00:00:00','0','','',0,0,'0','offline','zh-cn');
diff --git a/frontend/module/backup/control.php b/frontend/module/backup/control.php
index e82ffd7b..3643b089 100644
--- a/frontend/module/backup/control.php
+++ b/frontend/module/backup/control.php
@@ -174,7 +174,7 @@ public function backup($reload = 'no')
}
/**
- * Restore.
+ * Restore platform database and file.
*
* @param string $fileName
* @param string $confirm yes|no
@@ -493,7 +493,7 @@ public function ajaxUpgradePlatform()
$logExtra = array('result' => 'success', 'data' => array('oldVersion' => getenv('APP_VERSION'), 'newVersion' => $this->session->platformLatestVersion->version));
- $success = $this->loadModel('cne')->setPlatformVersion($this->session->platformLatestVersion->version);
+ $success = $this->loadModel('cne')->upgradePlatform($this->session->platformLatestVersion->version);
if($success)
{
$this->setting->setItem('system.backup.global.chartVersion', $this->session->platformLatestVersion->version);
diff --git a/frontend/module/backup/css/index.css b/frontend/module/backup/css/index.css
index 4968c8cc..f1f92208 100644
--- a/frontend/module/backup/css/index.css
+++ b/frontend/module/backup/css/index.css
@@ -1,15 +1,13 @@
.errorFiles {text-align: left; padding: 5px;}
.main-col table {margin-bottom: 30px;}
div.main-header {border-bottom: 0;}
+.main-header h2 {font-size: 16px;}
.btn-wide {width: 100px; margin: 0 10px;}
-#upgradeModal .modal-content {padding-bottom: 20px;}
-#upgradeModal .confirm-text{font-weight: 700;}
-#confirmRollback .modal-content {padding-bottom: 20px;}
-#confirmRollback .confirm-text{font-weight: 700;}
-div#mainContent {width: 60%; min-width: 600px; max-width: 1000px; margin: auto;}
+.modal .modal-content {padding-bottom: 20px;}
+.confirm-text{font-weight: 700;}
+div#mainContent {width: 900px; margin: auto;}
table.table a {vertical-align: baseline;}
-.info-header {height: 50px;}
+#mainContent .table tr{height: 40px;}
.history-header {height: 50px;}
-.info-row {height: 50px;}
.history-row {height: 50px;}
table.table-backup > tbody > tr > th {font-weight: normal;}
diff --git a/frontend/module/backup/js/index.js b/frontend/module/backup/js/index.js
index ee232257..e6e7873e 100644
--- a/frontend/module/backup/js/index.js
+++ b/frontend/module/backup/js/index.js
@@ -23,24 +23,45 @@ $(function()
});
}, 1000);
})
+
$(".restoreButton").click(function()
{
- $('#confirmRollback').modal('show');
+ let restoreUrl = $(this).data('url');
+ $('#confirmRestore #submitRestore').data('url', restoreUrl);
+ $('#confirmRestore').modal('show');
});
- $('.submitRestore').click(function()
+ $('#submitRestore').click(function()
{
- $('#confirmRollback').modal('hide');
+ let restoreUrl = $('#confirmRestore #submitRestore').data('url');
+ $('#hiddenwin').attr('src', restoreUrl);
+ $('#confirmRestore').modal('hide');
$('#restoring').modal('show');
setInterval(function()
{
$.get(createLink('backup', 'ajaxGetRestoreProgress'), function(data)
{
+ console.log(data);
data = JSON.parse(data);
$('.restoreSQL').text(data.sql);
$('.restoreFile').text(data.file);
});
}, 1000);
});
+
+ $(".deleteButton").click(function()
+ {
+ let delUrl = $(this).data('url');
+ $('#confirmDelete #submitDelete').data('url', delUrl);
+ $('#confirmDelete').modal('show');
+ });
+ $('#submitDelete').click(function()
+ {
+ $('#confirmDelete').modal('hide');
+ let delUrl = $('#confirmDelete #submitDelete').data('url');
+
+ $('#hiddenwin').attr('src', delUrl);
+ });
+
$('.rmPHPHeader').click(function()
{
$('#waitting .modal-body #backupType').html(rmPHPHeader);
@@ -48,29 +69,4 @@ $(function()
$('#waitting').modal('show');
})
- $('.restore').click(function()
- {
- url = $(this).attr('href');
- bootbox.confirm(confirmRestore, function(result)
- {
- if(result)
- {
- $('#waitting .modal-body #backupType').html(restore);
- $('#waitting .modal-content #message').hide();
- $('#waitting').modal('show');
-
- $.getJSON(url, function(response)
- {
- $('#waitting').modal('hide');
- bootbox.alert(response.message);
- });
- }
- else
- {
- return location.reload();
- }
- })
-
- return false;
- })
})
diff --git a/frontend/module/backup/lang/zh-cn.php b/frontend/module/backup/lang/zh-cn.php
index d458193d..8f0adbc4 100644
--- a/frontend/module/backup/lang/zh-cn.php
+++ b/frontend/module/backup/lang/zh-cn.php
@@ -60,8 +60,8 @@
$lang->backup->progressSQL = '备份中,已备份%s';
$lang->backup->progressAttach = '备份中,共有%s个文件,已经备份%s个';
$lang->backup->progressCode = '代码备份中,共有%s个文件,已经备份%s个';
-$lang->backup->confirmDelete = '是否删除备份?';
-$lang->backup->confirmRestore = '是否还原该备份?';
+$lang->backup->confirmDelete = '是否删除该备份?';
+$lang->backup->confirmRestore = '请确认是否回滚?';
$lang->backup->holdDays = '备份保留最近 %s 天';
$lang->backup->copiedFail = '复制失败的文件:';
$lang->backup->restoreTip = '还原功能只还原数据库。';
@@ -72,7 +72,6 @@
$lang->backup->restoreTitle = '正在回滚 渠成平台...';
$lang->backup->backingUp = '进行中';
$lang->backup->restoring = '进行中';
-$lang->backup->confirmRollback = '请确认是否回滚渠成平台?';
$lang->backup->success = new stdclass();
$lang->backup->success->backup = '备份成功!';
diff --git a/frontend/module/backup/model.php b/frontend/module/backup/model.php
index 6779802e..6a2dc393 100644
--- a/frontend/module/backup/model.php
+++ b/frontend/module/backup/model.php
@@ -158,7 +158,7 @@ public function restoreSQL($backupFile)
if($importResult && $importResult->result)
{
- $this->loadModel('instance')->deleteNotExist();
+ $this->loadModel('instance')->restoreInstanceList();
$this->processRestoreSummary('sql', 'done');
}
diff --git a/frontend/module/backup/view/index.html.php b/frontend/module/backup/view/index.html.php
index c5a37434..09bdb0b3 100644
--- a/frontend/module/backup/view/index.html.php
+++ b/frontend/module/backup/view/index.html.php
@@ -24,22 +24,20 @@
-
- backup->systemInfo;?>
-
+ backup->systemInfo;?>
-
backup->name;?> |
- backup->status;?> |
- backup->currentVersion;?> |
+ backup->status;?> |
+ backup->currentVersion;?> |
backup->latestVersion;?> |
- actions?> |
+ actions?> |
-
+
quchengPlatform;?> |
backup->running;?> |
|
@@ -58,9 +56,7 @@
-
- backup->history;?>
-
+
backup->history;?>
" . $lang->backup->setting, '', "data-width='500' class='iframe btn btn-primary'");?>
@@ -87,8 +83,11 @@
" . $lang->backup->restore, "id='restoreButton'", 'btn btn-link restoreButton');
- if(common::hasPriv('backup', 'delete')) echo html::a(inlink('delete', "file=$backupFile->name"), " " . $lang->delete, 'hiddenwin', "class='btn btn-link'");
+ $restoreUrl =inlink('restore', "file={$backupFile->name}&confirm=yes");
+ if(common::hasPriv('backup', 'restore')) echo html::commonButton(" {$lang->backup->restore}", "data-url='{$restoreUrl}'", 'btn btn-link restoreButton');
+
+ $deleteUrl = inlink('delete', "file={$backupFile->name}&confirm=yes");
+ if(common::hasPriv('backup', 'delete')) echo html::commonButton(" " . $lang->delete, "data-url='{$deleteUrl}'", 'btn btn-link deleteButton');
?>
|
@@ -159,22 +158,37 @@
-
+
backup->restore;?>
-
backup->confirmRollback;?>
+
backup->confirmRestore;?>
cancel, "data-dismiss='modal'", 'btn btn-wide');?>
- name}&confirm=yes"), $lang->confirm, 'hiddenwin', "class='btn btn-primary btn-wide submitRestore'");?>
+ confirm, "data-dismiss='modal' id='submitRestore'", 'btn btn-wide btn-primary');?>
+ name}&confirm=yes"), $lang->confirm, 'hiddenwin', "class='btn btn-primary btn-wide submitRestore'");?>
+
+
+
+
+
+
+
+
+
+
backup->delete;?>
+
backup->confirmDelete;?>
+
+
+ cancel, "data-dismiss='modal'", 'btn btn-wide');?>
+ confirm, "data-dismiss='modal' id='submitDelete'", 'btn btn-wide btn-primary');?>
-
diff --git a/frontend/module/cne/model.php b/frontend/module/cne/model.php
index 875d516d..79c8fd48 100644
--- a/frontend/module/cne/model.php
+++ b/frontend/module/cne/model.php
@@ -67,19 +67,59 @@ public function searchApps($keyword = '', $categories = array(), $page = 1, $pag
/**
* Get app info from cloud market.
*
- * @param int $id
+ * @param int $id
+ * @param boolean $analysis true: log this request for analysis.
+ * @param string $name
+ * @param string $version
+ * @param string $channel
* @access public
* @return object|null
*/
- public function getAppInfo($id)
+ public function getAppInfo($id, $analysis = false, $name = '', $version ='', $channel = '')
{
+ $apiParams = array();
+ $apiParams['analysis'] = $analysis ? 'true' : 'false' ;
+
+ if($id) $apiParams['id'] = $id;
+ if($name) $apiParams['name'] = $name;
+ if($version) $apiParams['version'] = $version;
+ if($channel) $apiParams['channel'] = $channel;
+
$apiUrl = '/api/market/appinfo';
- $result = $this->apiGet($apiUrl, array('id' => $id), $this->config->cloud->api->headers, $this->config->cloud->api->host);
+ $result = $this->apiGet($apiUrl, $apiParams, $this->config->cloud->api->headers, $this->config->cloud->api->host);
if(!isset($result->code) || $result->code != 200) return null;
return $result->data;
}
+ /**
+ * Get app version list to install.
+ *
+ * @param int $id
+ * @param string $name
+ * @param string $channel
+ * @param int $page
+ * @param int $pageSize
+ * @access public
+ * @return mixed
+ */
+ public function appVersionList($id, $name = '', $channel = '', $page = 1, $pageSize = 3)
+ {
+ $apiParams = array();
+ $apiParams['page'] = $page;
+ $apiParams['page_size'] = $pageSize;
+
+ if($id) $apiParams['id'] = $id;
+ if($name) $apiParams['name'] = $name;
+ if($channel) $apiParams['channel'] = $channel;
+
+ $apiUrl = '/api/market/app/version';
+ $result = $this->apiGet($apiUrl, $apiParams, $this->config->cloud->api->headers, $this->config->cloud->api->host);
+ if(!isset($result->code) || $result->code != 200) return null;
+
+ return array_combine(array_column($result->data, 'version'), $result->data);
+ }
+
/**
* Get upgradable versions of app from cloud market.
*
@@ -214,6 +254,38 @@ public function getCategories()
return $categories;
}
+ /**
+ * Get all instance of app in cluster.
+ *
+ * @access public
+ * @return array
+ */
+ public function instanceList()
+ {
+ $apiUrl = "/api/cne/system/app-full-list";
+ $result = $this->apiGet($apiUrl, array(), $this->config->CNE->api->headers);
+ if(empty($result) || $result->code != 200 || empty($result->data)) return array();
+
+ $instanceList = $result->data;
+ return array_combine(array_column($instanceList, 'name'), $instanceList);
+ }
+
+ /**
+ * Upgrade platform.
+ *
+ * @param string $toVersion
+ * @access public
+ * @return bool
+ */
+ public function upgradePlatform($toVersion)
+ {
+ $apiUrl = "/api/cne/system/update";
+ $result = $this->apiPost($apiUrl, array('version' => $toVersion, 'channel' => $this->config->CNE->api->channel), $this->config->CNE->api->headers);
+ if($result && $result->code == 200) return true;
+
+ return false;
+ }
+
/**
* Upgrade or degrade platform version.
*
@@ -645,6 +717,54 @@ public function queryStatus($instance)
return $result;
}
+ /**
+ * Get all database list of app.
+ *
+ * @param object $instance
+ * @access public
+ * @return mixed
+ */
+ public function appDBList($instance)
+ {
+ $apiUrl = "/api/cne/app/dbs";
+ $apiParams = array();
+ $apiParams['cluster'] = 'default';
+ $apiParams['name'] = $instance->k8name;
+ $apiParams['namespace'] = $instance->spaceData->k8space;
+ $apiParams['channel'] = $this->config->CNE->api->channel;
+
+ $result = $this->apiGet($apiUrl, $apiParams, $this->config->CNE->api->headers);
+ if(empty($result) || $result->code != 200 || empty($result->data)) return array();
+
+ $dbList = $result->data;
+ return array_combine(array_column($dbList, 'name'), $dbList);
+ }
+
+ /**
+ * Get detail of app database.
+ *
+ * @param object $instance
+ * @param string $dbName
+ * @access public
+ * @return null|object
+ */
+ public function appDBDetail($instance, $dbName)
+ {
+ $apiParams = array();
+ $apiParams['cluster'] = 'default';
+ $apiParams['name'] = $instance->k8name;
+ $apiParams['namespace'] = $instance->spaceData->k8space;
+ $apiParams['db'] = $dbName;
+ $apiParams['channel'] = $this->config->CNE->api->channel;
+
+ $apiUrl = "/api/cne/app/dbs/detail";
+
+ $result = $this->apiGet($apiUrl, $apiParams, $this->config->CNE->api->headers);
+ if(empty($result) || $result->code != 200 || empty($result->data)) return;
+
+ return $result->data;
+ }
+
/**
* Get database detail.
*
diff --git a/frontend/module/instance/config.php b/frontend/module/instance/config.php
index 585ddfd5..de11b3cb 100644
--- a/frontend/module/instance/config.php
+++ b/frontend/module/instance/config.php
@@ -2,3 +2,4 @@
$config->instance = new stdclass;
$config->instance->keepDomainList = array();
$config->instance->keepDomainList['console'] = 'console';
+$config->instance->keepDomainList['demo'] = 'demo';
diff --git a/frontend/module/instance/control.php b/frontend/module/instance/control.php
index d7136910..91e0ea35 100644
--- a/frontend/module/instance/control.php
+++ b/frontend/module/instance/control.php
@@ -54,6 +54,9 @@ public function view($id, $recTotal = 0, $recPerPage = 20, $pageID = 1, $tab ='b
$backupList = array();
if($tab == 'backup') $backupList = $this->instance->backupList($instance);
+ $dbList = new stdclass;
+ if($tab == 'advance') $dbList = $this->cne->appDBList($instance);
+
$this->view->position[] = $instance->appName;
$this->view->title = $instance->appName;
@@ -62,6 +65,7 @@ public function view($id, $recTotal = 0, $recPerPage = 20, $pageID = 1, $tab ='b
$this->view->defaultAccount = $this->cne->getDefaultAccount($instance);
$this->view->instanceMetric = $instanceMetric;
$this->view->backupList = $backupList;
+ $this->view->dbList = $dbList;
$this->view->tab = $tab;
$this->view->pager = $pager;
@@ -205,16 +209,20 @@ public function install($appID)
return $this->display('instance','resourceerror');
}
- $dbList = $this->cne->sharedDBList();
- $customData = new stdclass;
+ $versionList = $this->cne->appVersionList($cloudApp->id);
+ $dbList = $this->cne->sharedDBList();
+ $customData = new stdclass;
if(!empty($_POST))
{
$customData = fixer::input('post')
->trim('customName')->setDefault('customName', '')
->trim('customDomain')->setDefault('customDomain', null)
+ ->trim('version')->setDefault('version', '')
->trim('dbType')
->trim('dbService')
+ ->setDefault('app_version', '')
->get();
+ if($customData->version && isset($versionList[$customData->version])) $customData->app_version = $versionList[$customData->version]->app_version;
if(isset($this->config->instance->keepDomainList[$customData->customDomain]) || $this->instance->domainExists($customData->customDomain)) return $this->send(array('result' => 'fail', 'message' => $customData->customDomain . $this->lang->instance->errors->domainExists));
@@ -233,6 +241,8 @@ public function install($appID)
$this->view->title = $this->lang->instance->install . $cloudApp->alias;
$this->view->cloudApp = $cloudApp;
+
+ $this->view->versionList = array_combine(array_column($versionList, 'version'), array_column($versionList, 'app_version'));
$this->view->thirdDomain = $this->instance->randThirdDomain();
$this->view->dbList = $this->instance->dbListToOptions($dbList);
@@ -377,7 +387,9 @@ public function ajaxRestore()
if(empty($postData->instanceID) || empty($postData->backupName)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->wrongRequestData));
$instance = $this->instance->getByID($postData->instanceID);
+ if(empty($instance))return print(js::alert($this->lang->instance->instanceNotExists) . js::locate($this->createLink('space', 'browse')));
+ $this->instance->backup($instance, $this->app->user); // Backup automatically before restroe.
$success = $this->instance->restore($instance, $this->app->user, $postData->backupName);
if(!$success)
{
@@ -403,4 +415,34 @@ public function ajaxDeleteBackup($backupID)
return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'deleteSuccess')));
}
+
+ /**
+ * Generate database auth parameters and jump to login page.
+ *
+ * @access public
+ * @return void
+ */
+ public function ajaxDBAuthUrl()
+ {
+ $post = fixer::input('post')
+ ->setDefault('namespace', 'default')
+ ->setDefault('id', 0)
+ ->get();
+ if(empty($post->dbName)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->dbNameIsEmpty));
+
+ $instance = $this->instance->getByID($post->id);
+ if(empty($instance)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->instanceNotExists));
+
+ $detail = $this->loadModel('cne')->appDBDetail($instance, $post->dbName);
+ if(empty($detail)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->notFoundDB));
+
+ $dbAuth = array();
+ $dbAuth['server'] = $detail->host . ':' . $detail->port;
+ $dbAuth['username'] = $detail->username;
+ $dbAuth['db'] = $detail->database;
+ $dbAuth['password'] = $detail->password;
+
+ $url = '/adminer?' . http_build_query($dbAuth);
+ $this->send(array('result' => 'success', 'message' => '', 'data' => array('url' => $url)));
+ }
}
diff --git a/frontend/module/instance/css/view.css b/frontend/module/instance/css/view.css
index f162268c..e0ba0d9f 100644
--- a/frontend/module/instance/css/view.css
+++ b/frontend/module/instance/css/view.css
@@ -15,6 +15,9 @@
#backup .panel {padding: 10px;}
#backup .panel .btn-toolbar{padding: 0 0 10px 0;}
-#backup table th.actions{padding-left: 15px;}
+#backup table th.actions {padding-left: 15px;}
#backup table td>.btn {padding-left: 5px; padding-right: 5px;}
-#backup .btn.btn-info[disabled] { color: rgba(255,255,255,.7); background-color: #61be68;}
+#backup .btn.btn-info[disabled] {color: rgba(255,255,255,.7); background-color: #61be68;}
+
+#advance .panel-title span {padding-left: 10px;}
+#advance hr {margin: 5px 20px;}
diff --git a/frontend/module/instance/js/view.js b/frontend/module/instance/js/view.js
index 00f28b22..3c7da2d5 100644
--- a/frontend/module/instance/js/view.js
+++ b/frontend/module/instance/js/view.js
@@ -220,6 +220,25 @@ $(function()
});
});
+ $('button.db-login').on('click', function(event)
+ {
+ let dbName = $(event.target).data('db-name');
+ let id = $(event.target).data('id');
+
+ $.post(createLink('instance', 'ajaxDBAuthUrl'), {dbName, id}).done(function(res)
+ {
+ let response = JSON.parse(res);
+ if(response.result == 'success')
+ {
+ window.parent.open(response.data.url, 'Adminer');
+ }
+ else
+ {
+ bootbox.alert(response.message);
+ }
+ });
+ });
+
var enableTimer = true;
window.parent.$(window.parent.document).on('showapp', function(event, app)
{
diff --git a/frontend/module/instance/lang/zh-cn.php b/frontend/module/instance/lang/zh-cn.php
index 6d3f9dbc..a41bb203 100644
--- a/frontend/module/instance/lang/zh-cn.php
+++ b/frontend/module/instance/lang/zh-cn.php
@@ -9,9 +9,10 @@
$lang->instance->space = '空间';
$lang->instance->domain = '域名';
$lang->instance->dbType = '数据库';
-$lang->instance->advantageOption = '高级选项';
+$lang->instance->advanceOption = '高级选项';
$lang->instance->baseInfo = '基本信息';
$lang->instance->backupAndRestore = '备份';
+$lang->instance->advance = '高级';
$lang->instance->serviceInfo = '服务信息';
$lang->instance->appTemplate = '应用模板';
@@ -58,10 +59,11 @@
$lang->instance->backup->volSize = '大小';
$lang->instance->backup->statusList = array();
-$lang->instance->backup->statusList['success'] = '失败';
+$lang->instance->backup->statusList['success'] = '成功';
$lang->instance->backup->statusList['failed'] = '失败';
$lang->instance->backup->statusList['pending'] = '等待备份';
$lang->instance->backup->statusList['processing'] = '备份中';
+$lang->instance->backup->statusList['inprogress'] = '备份中';
$lang->instance->backup->statusList['completed'] = '完成备份';
$lang->instance->backup->statusList['executedFailed'] = '备份失败';
$lang->instance->backup->statusList['uploading'] = '上传中';
@@ -71,7 +73,7 @@
$lang->instance->restore = new stdclass;
$lang->instance->restore->statusList = array();
-$lang->instance->restore->statusList['success'] = '失败';
+$lang->instance->restore->statusList['success'] = '成功';
$lang->instance->restore->statusList['failed'] = '失败';
$lang->instance->restore->statusList['pending'] = '等待回滚';
$lang->instance->restore->statusList['processing'] = '回滚中';
@@ -80,6 +82,15 @@
$lang->instance->restore->statusList['downloading'] = '下载中';
$lang->instance->restore->statusList['downloadFailed'] = '下载失败';
+$lang->instance->dbList = '数据库';
+$lang->instance->dbName = '名称';
+$lang->instance->dbStatus = '状态';
+$lang->instance->dbType = '类型';
+$lang->instance->action = '操作';
+$lang->instance->management = '管理';
+$lang->instance->dbReady = '正常';
+$lang->instance->dbWaiting = '等待就绪';
+
$lang->instance->log = new stdclass;
$lang->instance->log->date = '日期';
$lang->instance->log->message = '内容';
@@ -202,3 +213,6 @@
$lang->instance->errors->restoreRunning = '当前回滚正在进行中,请等待当前回滚完成。';
$lang->instance->errors->noBackup = '无备份数据,';
$lang->instance->errors->wrongRequestData = '提交的数据有误,请刷新页面后重试。';
+$lang->instance->errors->noDBList = '无数据库或不可访问';
+$lang->instance->errors->notFoundDB = '找不到该数据库';
+$lang->instance->errors->dbNameIsEmpty = '数据库名为空';
diff --git a/frontend/module/instance/model.php b/frontend/module/instance/model.php
index 9d73a931..f4f70177 100644
--- a/frontend/module/instance/model.php
+++ b/frontend/module/instance/model.php
@@ -64,7 +64,7 @@ public function getByIdList($idList)
* @access public
* @return array
*/
- public function getByAccount($account = '', $pager = null, $pinned = '', $searchParam = '')
+ public function getByAccount($account = '', $pager = null, $pinned = '', $searchParam = '', $status = 'all')
{
$instances = $this->dao->select('instance.*')->from(TABLE_INSTANCE)->alias('instance')
->leftJoin(TABLE_SPACE)->alias('space')->on('space.id=instance.space')
@@ -72,6 +72,7 @@ public function getByAccount($account = '', $pager = null, $pinned = '', $search
->beginIF($account)->andWhere('space.owner')->eq($account)->fi()
->beginIF($pinned)->andWhere('instance.pinned')->eq((int)$pinned)->fi()
->beginIF($searchParam)->andWhere('instance.name')->like("%{$searchParam}%")->fi()
+ ->beginIF($status != 'all')->andWhere('instance.status')->eq($status)->fi()
->orderBy('instance.id desc')
->beginIF($pager)->page($pager)->fi()
->fetchAll('id');
@@ -340,8 +341,8 @@ public function install($app, $dbList, $customData, $spaceID = null)
$instanceData->source = 'cloud';
$instanceData->channel = $this->app->session->cloudChannel ? $this->app->session->cloudChannel : $this->config->cloud->api->channel;
$instanceData->chart = $app->chart;
- $instanceData->appVersion = $app->app_version;
- $instanceData->version = $app->version;
+ $instanceData->appVersion = $customData->app_version ? $customData->app_version : $app->app_version;
+ $instanceData->version = $customData->version;
$instanceData->space = $space->id;
$instanceData->k8name = $k8name;
$instanceData->status = 'creating';
@@ -357,6 +358,7 @@ public function install($app, $dbList, $customData, $spaceID = null)
$apiParams->namespace = $space->k8space;
$apiParams->name = $k8name;
$apiParams->chart = $app->chart;
+ $apiParams->version = $instance->version;
$apiParams->channel = $instance->channel;
$apiParams->settings_map = $this->installationSettingsMap($customData, $dbList, $app, $instance);
@@ -441,7 +443,7 @@ public function stop($instance)
}
/**
- * Upgrade app instnace to higher version.
+ * Upgrade app instance to higher version.
*
* @param object $instance
* @param string $toVersion
@@ -596,25 +598,77 @@ public function dbListToOptions($databases)
}
/**
- * Delete instances don't exist in CNE.
+ * Restore instance by data from k8s cluster.
*
* @access public
* @return void
*/
- public function deleteNotExist()
+ public function restoreInstanceList()
{
- $instances = $this->dao->select('*')->from(TABLE_INSTANCE)->where('deleted')->eq(0)->fetchAll('id');
+ $k8AppList = $this->cne->instancelist();
+ $k8NameList = array_keys($k8AppList);
- $spaces = $this->dao->select('*')->from(TABLE_SPACE)->where('id')->in(array_column($instances, 'space'))->fetchAll('id');
-
- foreach($instances as $instance)
+ //软删除不存在的数据
+ $this->dao->update(TABLE_INSTANCE)->set('deleted')->eq(1)->where('k8name')->notIn($k8NameList)->exec();
+ foreach($k8AppList as $k8App)
{
- $instance->spaceData = zget($spaces, $instance->space, new stdclass);
+ $existInstance = $this->dao->select('id')->from(TABLE_INSTANCE)->where('k8name')->eq($k8App->name)->fetch();
+ if($existInstance) continue;
+
+ $marketApp = $this->cne->getAppInfo(0, false, $k8App->chart, $k8App->version, $k8App->channel);
+ if(empty($marketApp)) continue;
+
+ $instanceData = new stdclass;
+ $instanceData->appId = $marketApp->id;
+ $instanceData->appName = $marketApp->alias;
+ $instanceData->name = $marketApp->alias;
+ $instanceData->logo = $marketApp->logo;
+ $instanceData->desc = $marketApp->desc;
+ $instanceData->introduction = isset($marketApp->introduction) ? $marketApp->introduction : $marketApp->desc;
+ $instanceData->source = 'cloud';
+ $instanceData->channel = $k8App->channel;
+ $instanceData->chart = $k8App->chart;
+ $instanceData->appVersion = $marketApp->app_version;
+ $instanceData->version = $k8App->version;
+ $instanceData->k8name = $k8App->name;
+ $instanceData->status = 'stopped';
+
+ $parsedK8Name = $this->parseK8Name($k8App->name);
+
+ $instanceData->createdBy = $k8App->username ? $k8App->username : $parsedK8Name->createdBy;
+ $instanceData->createdAt = $parsedK8Name->createdAt;
+
+ $space = $this->dao->select('id,k8space')->from(TABLE_SPACE)->where('k8space')->eq($k8App->namespace)->fetch();
+ if(empty($space)) $space = $this->loadModel('space')->defaultSpace($instanceData->createdBy);
+
+ $instanceData->space = $space ? $space->id : $defaultSpace->id;
+
+ $this->dao->insert(TABLE_INSTANCE)->data($instanceData)->exec();
+ }
+ }
+
+ /**
+ * Parse K8Name to get more data: chart, created time, user name.
+ *
+ * @param string $k8Name
+ * @access public
+ * @return mixed
+ */
+ public function parseK8Name($k8Name)
+ {
+ $datePosition = strripos($k8Name, '-');
+ $createdAt = trim(substr($k8Name, $datePosition), '-');
- $statusResponse = $this->cne->queryStatus($instance);
+ $createdBy = trim(substr($k8Name, 0, $datePosition), '-');
+ $accountPosition = strripos($createdBy, '-');
+ $createdBy = trim(substr($createdBy, $accountPosition), '-');
- if($statusResponse->code == 404) $this->softDeleteByID($instance->id);
- }
+ $parsedData = new stdclass;
+ $parsedData->chart = trim(substr($k8Name, 0, $accountPosition));
+ $parsedData->createdBy = trim($createdBy, '-');
+ $parsedData->createdAt = date('Y-m-d H:i:s', strtotime($createdAt));
+
+ return $parsedData;
}
/**
@@ -767,6 +821,22 @@ public function printRestoreBtn($instance, $backup)
echo $btn;
}
+ /**
+ * Print button for managing database.
+ *
+ * @param object $db
+ * @param object $instance
+ * @access public
+ * @return void
+ */
+ public function printDBAction($db, $instance)
+ {
+ $disabled = $db->ready ? '' : 'disabled';
+ $btnHtml = html::commonButton($this->lang->instance->management, "{$disabled} data-db-name='{$db->name}' data-id='{$instance->id}'", 'db-login btn btn-primary');
+
+ echo $btnHtml;
+ }
+
/**
* Print message of action log of instance.
*
diff --git a/frontend/module/instance/view/advance.html.php b/frontend/module/instance/view/advance.html.php
new file mode 100644
index 00000000..c372f27a
--- /dev/null
+++ b/frontend/module/instance/view/advance.html.php
@@ -0,0 +1,34 @@
+
+
+
instance->domain;?>:domain;?>
+
+
+
+
+
+
+
+
+ instance->dbName?> |
+ instance->dbType;?> |
+ instance->dbStatus;?> |
+ instance->action?> |
+
+
+
+
+
+ db_name;?> |
+ db_type?> |
+ ready ? $lang->instance->dbReady : $lang->instance->dbWaiting;?> |
+ instance->printDBAction($db, $instance);?> |
+
+
+
+
+
+
+
+
diff --git a/frontend/module/instance/view/backup.html.php b/frontend/module/instance/view/backup.html.php
index e9ba1ec2..771a4427 100644
--- a/frontend/module/instance/view/backup.html.php
+++ b/frontend/module/instance/view/backup.html.php
@@ -34,16 +34,16 @@
create_time);?> |
username;?> |
- backup_details->db[0], 'db_type');?> |
- backup_details->db[0], 'db_name');?> |
- instance->backup->statusList, strtolower(zget($backup->backup_details->db[0], 'status')));?> |
- backup_details->db[0], 'cost');?> |
- backup_details->db[0], 'size') / 1024);?> |
- backup_details->volume[0], 'pvc_name');?> |
- backup_details->volume[0], 'volume');?> |
- instance->backup->statusList, strtolower(zget($backup->backup_details->volume[0], 'status')));?> |
- backup_details->volume[0], 'cost');?> |
- backup_details->volume[0], 'doneBytes') / 1024);?> |
+ backup_details->db[0]) ? zget($backup->backup_details->db[0], 'db_type') : '';?> |
+ backup_details->db[0]) ? zget($backup->backup_details->db[0], 'db_name') : '';?> |
+ backup_details->db[0]) ? zget($lang->instance->backup->statusList, strtolower(zget($backup->backup_details->db[0], 'status'))) : '';?> |
+ backup_details->db[0]) ? zget($backup->backup_details->db[0], 'cost') : '';?> |
+ backup_details->db[0]) ? helper::formatKB(zget($backup->backup_details->db[0], 'size') / 1024) : '';?> |
+ backup_details->volume[0]) ? zget($backup->backup_details->volume[0], 'pvc_name') : '';?> |
+ backup_details->volume[0]) ? zget($backup->backup_details->volume[0], 'volume') : '';?> |
+ backup_details->volume[0]) ? zget($lang->instance->backup->statusList, strtolower(zget($backup->backup_details->volume[0], 'status'))) : '';?> |
+ backup_details->volume[0]) ? zget($backup->backup_details->volume[0], 'cost') : '';?> |
+ backup_details->volume[0]) ? helper::formatKB(zget($backup->backup_details->volume[0], 'doneBytes') / 1024) : '';?> |
instance->printRestoreBtn($instance, $backup);?> |
diff --git a/frontend/module/instance/view/baseinfo.html.php b/frontend/module/instance/view/baseinfo.html.php
index 4a10a625..7296cc0b 100644
--- a/frontend/module/instance/view/baseinfo.html.php
+++ b/frontend/module/instance/view/baseinfo.html.php
@@ -1,7 +1,7 @@
-
instance->serviceInfo?>
+
instance->serviceInfo;?>
diff --git a/frontend/module/instance/view/install.html.php b/frontend/module/instance/view/install.html.php
index ae77f6d8..c0e6a027 100644
--- a/frontend/module/instance/view/install.html.php
+++ b/frontend/module/instance/view/install.html.php
@@ -21,8 +21,8 @@
- dependencies->mysql)):?>
-
instance->advantageOption . "", '', "data-toggle='collapse'");?>
+
instance->advanceOption . "", '', "data-toggle='collapse'");?>
-
instance->install);?>
diff --git a/frontend/module/instance/view/view.html.php b/frontend/module/instance/view/view.html.php
index 08d20e89..85611548 100644
--- a/frontend/module/instance/view/view.html.php
+++ b/frontend/module/instance/view/view.html.php
@@ -27,8 +27,9 @@
app->getModuleRoot() . '/common/view/footer.html.php';?>
diff --git a/frontend/module/navigation/control.php b/frontend/module/navigation/control.php
index 394aa130..7d633321 100644
--- a/frontend/module/navigation/control.php
+++ b/frontend/module/navigation/control.php
@@ -11,41 +11,138 @@
*/
class navigation extends control
{
+ /**
+ * Create a app.
+ *
+ * @access public
+ * @return void
+ */
+ public function create()
+ {
+ if($_POST)
+ {
+ $this->navigation->create();
+ if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('browse')));
+ }
+
+ $this->display();
+ }
+
+ /**
+ * The main page of navigation.
+ *
+ * @access public
+ * @return void
+ */
public function browse()
{
$this->loadModel('instance');
$account = $this->app->user->account;
- $instances = $this->instance->getByAccount($account);
- $pinnedInstances = $this->instance->getByAccount($account, '', true);
+ $hideInaccessible = $this->navigation->getSetting('hideInaccessible');
+ $status = $hideInaccessible->value == 'on' ? 'running' : 'all';
+
+ $instances = $this->instance->getByAccount($account);
+ $pinnedInstances = $this->instance->getByAccount($account, '', true, '', $status);
+ $apps = $this->navigation->getApps();
+ $pinnedApps = $this->navigation->getApps(true);
$this->view->title = $this->lang->navigation->common;
$this->view->instances = $instances;
$this->view->pinnedInstances = $pinnedInstances;
+ $this->view->apps = $apps;
+ $this->view->pinnedApps = $pinnedApps;
$this->display();
}
- public function ajaxGetPinnedInstance($id)
+ /**
+ * Ajax get pinned instances and pinned apps.
+ *
+ * @param int $id
+ * @param string $objectType
+ * @access public
+ * @return void
+ */
+ public function ajaxGetPinnedInstance($id, $objectType = 'instance')
{
$this->loadModel('instance');
- $this->instance->pinToggle($id);
+ if($objectType == 'app')
+ {
+ $this->navigation->pinToggle($id);
+ }
+ else
+ {
+ $this->instance->pinToggle($id);
+ }
$account = $this->app->user->account;
- $this->view->pinnedInstances = $this->instance->getByAccount($account, '', true);
+ $hideInaccessible = $this->navigation->getSetting('hideInaccessible');
+ $status = $hideInaccessible->value == 'on' ? 'running' : 'all';
+
+ $this->view->pinnedInstances = $this->instance->getByAccount($account, '', true, '', $status);
+ $this->view->pinnedApps = $this->navigation->getApps(true);
$this->view->showAddItem = true;
$this->display();
}
+ /**
+ * Ajax search pinned instances and pinned apps.
+ *
+ * @param string $name
+ * @access public
+ * @return void
+ */
public function ajaxSearchPinnedInstance($name = '')
{
$name = base64_decode(trim($name));
$this->loadModel('instance');
$account = $this->app->user->account;
- $this->view->pinnedInstances = $this->instance->getByAccount($account, '', true, $name);
+
+ $hideInaccessible = $this->navigation->getSetting('hideInaccessible');
+ $status = $hideInaccessible->value == 'on' ? 'running' : 'all';
+
+ $this->view->pinnedInstances = $this->instance->getByAccount($account, '', true, $name, $status);
+ $this->view->pinnedApps = $this->navigation->getApps(true, $name);
$this->view->showAddItem = false;
$this->display('navigation', 'ajaxGetPinnedInstance');
}
+
+ /**
+ * The settings page of navigation.
+ *
+ * @access public
+ * @return void
+ */
+ public function settings()
+ {
+ $this->view->settings = $this->navigation->getSettings();
+ $this->display();
+ }
+
+ /**
+ * Configure settings for navigation.
+ *
+ * @param string $field
+ * @access public
+ * @return void
+ */
+ public function configure($field)
+ {
+ if($_POST)
+ {
+ $this->navigation->configure($field);
+ if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
+ return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('settings')));
+ }
+
+ $hideInaccessible = $this->navigation->getSetting('hideInaccessible');
+
+ $this->view->field = $field;
+ $this->view->value = $hideInaccessible->value;
+ $this->display();
+ }
}
diff --git a/frontend/module/navigation/css/browse.css b/frontend/module/navigation/css/browse.css
index d40c8286..7d347e8e 100644
--- a/frontend/module/navigation/css/browse.css
+++ b/frontend/module/navigation/css/browse.css
@@ -4,7 +4,7 @@
#app main {flex-direction: column;}
#app main, #app #sortable {padding: 30px 10px; display: flex; justify-content: center; align-items: center; flex: 1; position: relative; flex-wrap: wrap; align-content: center; list-style: none; margin: 0;}
#config-buttons {position: fixed; bottom: 0; right: 0; display: flex; flex-direction: column;}
-#config-buttons a {text-align: center; line-height: 50px; color: white; margin-top: 1px;}
+#config-buttons a {text-align: center; line-height: 30px; color: white; margin-top: 1px;}
.item-container {position: relative;}
#app.sidebar nav {left: 0;}
.add-item {width: 280px; height: 90px; margin: 20px; flex: 0 0 280px; border-radius: 6px; padding: 20px; border: 4px dashed rgba(255, 255, 255, 0.7); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3); color: white; overflow: hidden; position: relative; display: none; outline: 1px solid transparent;}
diff --git a/frontend/module/navigation/css/configure.css b/frontend/module/navigation/css/configure.css
new file mode 100644
index 00000000..cc34cef2
--- /dev/null
+++ b/frontend/module/navigation/css/configure.css
@@ -0,0 +1,41 @@
+article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {display: block;}
+#app main {flex-direction: column;}
+#app {display: flex; min-height: 100vh; flex-direction: column; background-image: url("/background.jpg"); background-repeat: no-repeat; background-size: cover; background-position: bottom center;}
+#app main, #app #sortable {padding: 30px 10px; display: flex; justify-content: center; align-items: center; flex: 1; position: relative; flex-wrap: wrap; align-content: center; list-style: none; margin: 0;}
+#app .content {flex-grow: 1; display: flex; flex-direction: column;}
+.module-container {box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.4); border: 1px solid #cdced8; background: #f9fafd; max-width: 1000px; width: 100%; margin: 10px 40px; border-radius: 5px; overflow: hidden;}
+.module-container header, .module-container footer {display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #fff; background: #f2f3f6; font-size: 16px; border-bottom: 1px solid #dbdce3; height: 60px; position: relative;}
+.module-container header .section-title, .module-container footer .section-title {font-size: 18px; color: #5b5b5b; margin-left: 25px;}
+.module-container footer {border-top: 1px solid #dbdce3;}
+.module-actions {display: flex; justify-content: space-between; align-items: center;}
+.module-actions .button {font-size: 18px; color: #515564; padding: 0 10px; border: none; border-left: 1px solid #cdced8; display: flex; line-height: 1; position: relative; background: transparent; flex-direction: column; justify-content: center; align-items: center; min-width: 65px; height: 60px; text-decoration: none; box-sizing: border-box;}
+.module-actions .button:after {position: absolute; content: ""; top: 0; left: 0; bottom: 0; border-right: 1px solid #fff;}
+.module-actions .button span {display: inline-block; line-height: 1; font-size: 9px; font-weight: 400; text-transform: uppercase; color: #ababab; position: relative; top: 4px; margin: 0;}
+.module-container footer {border-top: 1px solid #dbdce3;}
+.module-container .table {width: 100%; margin: 0; background: #fff;}
+.module-container .table tbody tr:hover {background: #fefbf2;}
+.module-container .table tbody tr:hover td:first-child {position: relative;}
+.module-container .table tbody tr:hover td:first-child:before {content: ""; position: absolute; top: 0; left: 0; bottom: 0; width: 5px; background: #0eb584;}
+.module-container .table tbody td {padding: 20px 25px; font-size: 13px; color: #2f313a; max-width: 500px; word-break: break-word;}
+.module-container .table tbody td.form-error {background: #e69191; color: white; text-align: center;}
+.module-container .table tbody a {color: #2f313a;}
+div.create {padding: 30px 15px; display: flex; flex-wrap: wrap;}
+div.create .input {width: 280px; margin: 20px;}
+div.create .input label:not(.switch) {width: 100%; font-size: 13px; color: #9094a5; margin-bottom: 15px; display: block; font-weight: 300;}
+div.create .input input, div.create .input select {width: 100%; border: 1px solid #dedfe2; padding: 10px; border-radius: 6px;}
+#config-buttons {position: fixed; bottom: 0; right: 0; display: flex; flex-direction: column;}
+#config-buttons a img {width: 26px; height: 26px; margin-top: 12px;}
+#config-buttons a {text-align: center; line-height: 30px; color: white; margin-top: 1px;}
+
+/* The slider */
+.switch input {display: none;}
+.config-slider {position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #4a556b; transition: 0.4s;}
+.config-slider:before {position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: 0.4s;}
+input:checked + .config-slider {background-color: #2196F3;}
+input:focus + .config-slider {box-shadow: 0 0 1px #2196F3;}
+input:checked + .config-slider:before {transform: translateX(16px);}
+.switch {position: relative; display: inline-block; width: 36px; height: 20px;}
+
+/* Rounded sliders */
+.config-slider.round {border-radius: 20px;}
+.config-slider.round:before {border-radius: 50%;}
diff --git a/frontend/module/navigation/css/create.css b/frontend/module/navigation/css/create.css
new file mode 100644
index 00000000..0e4674a7
--- /dev/null
+++ b/frontend/module/navigation/css/create.css
@@ -0,0 +1,33 @@
+article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {display: block;}
+#app main {flex-direction: column;}
+#app {display: flex; min-height: 100vh; flex-direction: column; background-image: url("/background.jpg"); background-repeat: no-repeat; background-size: cover; background-position: bottom center;}
+#app main, #app #sortable {padding: 30px 10px; display: flex; justify-content: center; align-items: center; flex: 1; position: relative; flex-wrap: wrap; align-content: center; list-style: none; margin: 0;}
+#app .content {flex-grow: 1; display: flex; flex-direction: column;}
+.module-container {box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.4); border: 1px solid #cdced8; background: #f9fafd; max-width: 1000px; width: 100%; margin: 10px 40px; border-radius: 5px; overflow: hidden;}
+.module-container header, .module-container footer {display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #fff; background: #f2f3f6; font-size: 16px; border-bottom: 1px solid #dbdce3; height: 60px; position: relative;}
+.module-container header .section-title, .module-container footer .section-title {font-size: 18px; color: #5b5b5b; margin-left: 25px;}
+.module-container footer {border-top: 1px solid #dbdce3;}
+.module-actions {display: flex; justify-content: space-between; align-items: center;}
+.module-actions .button {font-size: 18px; color: #515564; padding: 0 10px; border: none; border-left: 1px solid #cdced8; display: flex; line-height: 1; position: relative; background: transparent; flex-direction: column; justify-content: center; align-items: center; min-width: 65px; height: 60px; text-decoration: none; box-sizing: border-box;}
+.module-actions .button:after {position: absolute; content: ""; top: 0; left: 0; bottom: 0; border-right: 1px solid #fff;}
+.module-actions .button span {display: inline-block; line-height: 1; font-size: 9px; font-weight: 400; text-transform: uppercase; color: #ababab; position: relative; top: 4px; margin: 0;}
+.module-container footer {border-top: 1px solid #dbdce3;}
+.module-container .table {width: 100%; margin: 0; background: #fff;}
+.module-container .table tbody tr:hover {background: #fefbf2;}
+.module-container .table tbody tr:hover td:first-child {position: relative;}
+.module-container .table tbody tr:hover td:first-child:before {content: ""; position: absolute; top: 0; left: 0; bottom: 0; width: 5px; background: #0eb584;}
+.module-container .table tbody td {padding: 20px 25px; font-size: 13px; color: #2f313a; max-width: 500px; word-break: break-word;}
+.module-container .table tbody td.form-error {background: #e69191; color: white; text-align: center;}
+.module-container .table tbody a {color: #2f313a;}
+div.create {padding: 30px 15px; display: flex; flex-wrap: wrap;}
+div.create .input {width: 280px; margin: 20px;}
+div.create .input label:not(.switch) {width: 100%; font-size: 13px; color: #9094a5; margin-bottom: 15px; display: block; font-weight: 300;}
+div.create .input input, div.create .input select {width: 100%; border: 1px solid #dedfe2; padding: 10px; border-radius: 6px;}
+
+.create .textarea {width: 100%; margin: 0px 20px;}
+.create .textarea textarea {width: 100%; border: 1px solid #dedfe2; padding: 15px; border-radius: 6px; height: 100px; font-size: 14px;}
+.create .textarea label:not(.switch) {width: 100%; font-size: 13px; color: #9094a5; margin-bottom: 15px; display: block; font-weight: 300;}
+
+#config-buttons {position: fixed; bottom: 0; right: 0; display: flex; flex-direction: column;}
+#config-buttons a img {width: 26px; height: 26px; margin-top: 12px;}
+#config-buttons a {text-align: center; line-height: 30px; color: white; margin-top: 1px;}
diff --git a/frontend/module/navigation/css/settings.css b/frontend/module/navigation/css/settings.css
new file mode 100644
index 00000000..811a06a3
--- /dev/null
+++ b/frontend/module/navigation/css/settings.css
@@ -0,0 +1,19 @@
+header, main, nav, section, summary {display: block;}
+#app {display: flex; min-height: 100vh; flex-direction: column; background-image: url("/background.jpg"); background-repeat: no-repeat; background-size: cover; background-position: bottom center;}
+#app .content {flex-grow: 1; display: flex; flex-direction: column;}
+#app main {flex-direction: column;}
+#app main, #app #sortable {padding: 30px 10px; display: flex; justify-content: center; align-items: center; flex: 1; position: relative; flex-wrap: wrap; align-content: center; list-style: none; margin: 0;}
+.module-container {box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.4); border: 1px solid #cdced8; background: #f9fafd; max-width: 1000px; width: 100%; margin: 10px 40px; border-radius: 5px; overflow: hidden;}
+
+#config-buttons {position: fixed; bottom: 0; right: 0; display: flex; flex-direction: column;}
+#config-buttons a {text-align: center; line-height: 30px; color: white; margin-top: 1px;}
+
+.module-container .table {width: 100%; margin: 0; background: #fff;}
+.module-container .table thead th {background: #f2f3f6; color: #767d94; border-top: 1px solid #fff; text-align: left; font-size: 13px; text-transform: uppercase; padding: 15px 25px;}
+.module-container .table tbody tr:hover {background: #fefbf2;}
+.module-container .table tbody tr:hover td:first-child {position: relative;}
+.module-container .table tbody tr:hover td:first-child:before {content: ""; position: absolute; top: 0; left: 0; bottom: 0; width: 5px; background: #0eb584;}
+.module-container .table tbody td {padding: 20px 25px; font-size: 13px; color: #2f313a; max-width: 500px; word-break: break-word;}
+.module-container .table tbody td.form-error {background: #e69191; color: white; text-align: center;}
+.module-container .table tbody a {color: #2f313a;}
+.module-container header .section-title, .module-container footer .section-title {font-size: 18px; color: #5b5b5b; margin-left: 25px;}
diff --git a/frontend/module/navigation/js/browse.js b/frontend/module/navigation/js/browse.js
index 677b62ec..4b47d76a 100644
--- a/frontend/module/navigation/js/browse.js
+++ b/frontend/module/navigation/js/browse.js
@@ -71,7 +71,14 @@ $('.instance-item').click(function()
$(this).addClass('active');
}
instanceID = $(this).attr('data-id');
- link = createLink('navigation', 'ajaxGetPinnedInstance', 'instanceID=' + instanceID);
+ if($(this).hasClass('app-item'))
+ {
+ link = createLink('navigation', 'ajaxGetPinnedInstance', 'instanceID=' + instanceID + '&objectType=app');
+ }
+ else
+ {
+ link = createLink('navigation', 'ajaxGetPinnedInstance', 'instanceID=' + instanceID);
+ }
$.get(link, function(result)
{
$('#sortable').replaceWith(result);
diff --git a/frontend/module/navigation/lang/zh-cn.php b/frontend/module/navigation/lang/zh-cn.php
index 054970d3..4396168d 100644
--- a/frontend/module/navigation/lang/zh-cn.php
+++ b/frontend/module/navigation/lang/zh-cn.php
@@ -1,5 +1,17 @@
navigation = new stdclass;
-$lang->navigation->common = '导航';
+$lang->navigation->common = '导航';
$lang->navigation->pinInstance = '固定服务到导航页';
-$lang->navigation->search = '搜索';
+$lang->navigation->search = '搜索';
+$lang->navigation->addApp = '添加服务';
+$lang->navigation->url = 'URL';
+$lang->navigation->appName = '服务名称';
+$lang->navigation->label = '设置项';
+$lang->navigation->value = '值';
+
+$lang->navigation->settings = new stdclass();
+$lang->navigation->settings->common = '设置';
+$lang->navigation->settings->hideInaccessible = '隐藏未启动的服务';
+$lang->navigation->settings->on = '开启';
+$lang->navigation->settings->off = '关闭';
+
diff --git a/frontend/module/navigation/model.php b/frontend/module/navigation/model.php
new file mode 100644
index 00000000..543be362
--- /dev/null
+++ b/frontend/module/navigation/model.php
@@ -0,0 +1,123 @@
+
+ * @package navigation
+ * @version $Id$
+ * @link https://www.qucheng.com
+ */
+class navigationModel extends model
+{
+ /**
+ * The construct of navigation model.
+ *
+ * @access public
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Get a app by id.
+ *
+ * @param int $id
+ * @access public
+ * @return object
+ */
+ public function getByID($id)
+ {
+ return $this->dao->select('*')->from(TABLE_NAVINSTANCE)->where('id')->eq($id)->fetch();
+ }
+
+ /**
+ * Create a app.
+ *
+ * @access public
+ * @return void
+ */
+ public function create()
+ {
+ $app = fixer::input('post')
+ ->setDefault('pinned', 0)
+ ->setDefault('createdBy', $this->app->user->account)
+ ->setDefault('createdAt', helper::now())
+ ->get();
+
+ $this->dao->insert(TABLE_NAVINSTANCE)->data($app)->autoCheck()->check('title', 'notempty')->exec();
+ }
+
+ /**
+ * Get apps.
+ *
+ * @param string $pinned
+ * @param string $searchParam
+ * @access public
+ * @return array
+ */
+ public function getApps($pinned = '', $searchParam = '')
+ {
+ $apps = $this->dao->select('*')->from(TABLE_NAVINSTANCE)
+ ->beginIF($pinned)->where('pinned')->eq((int)$pinned)->fi()
+ ->beginIF($searchParam)->andWhere('title')->like("%{$searchParam}%")->fi()
+ ->fetchAll();
+ return $apps;
+ }
+
+ /**
+ * Toggle pinned or unpinned.
+ *
+ * @param int $appID
+ * @access public
+ * @return void
+ */
+ public function pinToggle($appID)
+ {
+ $app = $this->getByID($appID);
+ $pinned = $app->pinned == '0' ? '1' : '0';
+ $this->dao->update(TABLE_NAVINSTANCE)->set('pinned')->eq($pinned)->where('id')->eq($appID)->exec();
+ }
+
+ /**
+ * Get all navigation's setting.
+ *
+ * @access public
+ * @return array
+ */
+ public function getSettings()
+ {
+ $settings = $this->dao->select('*')->from(TABLE_CONFIG)->where('module')->eq('navigation')->fetchAll();
+ return $settings;
+ }
+
+ /**
+ * Get a setting of navigation.
+ *
+ * @param string $field
+ * @access public
+ * @return object
+ */
+ public function getSetting($field)
+ {
+ return $this->dao->select('value')->from(TABLE_CONFIG)->where('module')->eq('navigation')->andWhere('`key`')->eq($field)->fetch();
+ }
+
+ /**
+ * Change a setting.
+ *
+ * @param string $field
+ * @access public
+ * @return void
+ */
+ public function configure($field)
+ {
+ $post = fixer::input('post')->get();
+ $value = $post->value == '1' ? 'on' : 'off';
+
+ $this->loadModel('setting')->setItem('system.navigation.global.hideInaccessible', $value);
+ }
+}
diff --git a/frontend/module/navigation/view/ajaxgetpinnedinstance.html.php b/frontend/module/navigation/view/ajaxgetpinnedinstance.html.php
index 63edf863..10c0f0a6 100644
--- a/frontend/module/navigation/view/ajaxgetpinnedinstance.html.php
+++ b/frontend/module/navigation/view/ajaxgetpinnedinstance.html.php
@@ -4,8 +4,6 @@
#app .content {flex-grow: 1; display: flex; flex-direction: column;}
#app main {flex-direction: column;}
#app main, #app #sortable {padding: 30px 10px; display: flex; justify-content: center; align-items: center; flex: 1; position: relative; flex-wrap: wrap; align-content: center; list-style: none; margin: 0;}
-#config-buttons {position: fixed; bottom: 0; right: 0; display: flex; flex-direction: column;}
-#config-buttons a {text-align: center; line-height: 50px; color: white; margin-top: 1px;}
.item-container {position: relative;}
#app.sidebar nav {left: 0;}
.add-item {width: 280px; height: 90px; margin: 20px; flex: 0 0 280px; border-radius: 6px; padding: 20px; border: 4px dashed rgba(255, 255, 255, 0.7); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3); color: white; overflow: hidden; position: relative; display: none; outline: 1px solid transparent;}
@@ -52,6 +50,21 @@
+
+
+
+
+ logo) echo html::image($app->logo, "class='instance-logo'");?>
+
+
+
+
+
+
lang->navigation->pinInstance;?>
diff --git a/frontend/module/navigation/view/browse.html.php b/frontend/module/navigation/view/browse.html.php
index f5e84c64..fa8f8c84 100644
--- a/frontend/module/navigation/view/browse.html.php
+++ b/frontend/module/navigation/view/browse.html.php
@@ -1,6 +1,6 @@
pinned) == true ? 'active' : '';?>
name;?>
+
+ pinned) == true ? 'active' : '';?>
+ title;?>
+
@@ -47,6 +51,19 @@
+
+
+
+
+ logo) echo html::image($app->logo, "class='instance-logo'");?>
+
+
+
+
diff --git a/frontend/module/navigation/view/configure.html.php b/frontend/module/navigation/view/configure.html.php
new file mode 100644
index 00000000..e7910745
--- /dev/null
+++ b/frontend/module/navigation/view/configure.html.php
@@ -0,0 +1,47 @@
+
+ * @package navigation
+ * @version $Id$
+ * @link https://www.qucheng.com
+ */
+?>
+
+
+
+
+
+
+ createLink('navigation', 'browse'), '');?>
+ createLink('navigation', 'settings'), '');?>
+
+
+
+
+
diff --git a/frontend/module/navigation/view/create.html.php b/frontend/module/navigation/view/create.html.php
new file mode 100644
index 00000000..87fda27e
--- /dev/null
+++ b/frontend/module/navigation/view/create.html.php
@@ -0,0 +1,49 @@
+
+ * @package navigation
+ * @version $Id$
+ * @link https://www.qucheng.com
+ */
+?>
+
+
+
+
+
+
+ createLink('navigation', 'browse'), '');?>
+ createLink('navigation', 'settings'), '');?>
+
+
+
+
+
diff --git a/frontend/module/navigation/view/settings.html.php b/frontend/module/navigation/view/settings.html.php
new file mode 100644
index 00000000..e130631f
--- /dev/null
+++ b/frontend/module/navigation/view/settings.html.php
@@ -0,0 +1,47 @@
+
+ * @package navigation
+ * @version $Id$
+ * @link https://www.qucheng.com
+ */
+?>
+
+
+
+
+
+
+
+
+
+ lang->navigation->label;?> |
+ lang->navigation->value;?> |
+ |
+
+
+
+
+
+ lang->navigation->settings, $setting->key);?> |
+ lang->navigation->settings, $setting->value);?> |
+ createLink('navigation', 'configure', 'field=hideInaccessible'), "");?> |
+
+
+
+
+
+
+ createLink('navigation', 'browse'), '');?>
+ createLink('navigation', 'settings'), '');?>
+
+
+
+
+
diff --git a/frontend/module/space/control.php b/frontend/module/space/control.php
index 85f4da20..093b1870 100644
--- a/frontend/module/space/control.php
+++ b/frontend/module/space/control.php
@@ -23,7 +23,7 @@ class space extends control
@access public
* @return void
*/
- public function browse($spaceID = null, $browseType = 'all', $recTotal = 0, $recPerPage = 20, $pageID = 1)
+ public function browse($spaceID = null, $browseType = 'all', $recTotal = 0, $recPerPage = 24, $pageID = 1)
{
$this->app->loadLang('instance');
$this->loadModel('instance');
diff --git a/frontend/module/space/view/browsebycard.html.php b/frontend/module/space/view/browsebycard.html.php
index 96b26274..3179ae40 100644
--- a/frontend/module/space/view/browsebycard.html.php
+++ b/frontend/module/space/view/browsebycard.html.php
@@ -59,6 +59,6 @@
-
+
diff --git a/frontend/module/store/control.php b/frontend/module/store/control.php
index a0e7242a..f3007cfd 100644
--- a/frontend/module/store/control.php
+++ b/frontend/module/store/control.php
@@ -97,7 +97,7 @@ public function browse($recTotal = 0, $recPerPage = 0, $pageID = 1, $channel = '
*/
public function appView($id)
{
- $appInfo = $this->cne->getAppInfo($id);
+ $appInfo = $this->cne->getAppInfo($id, true);
if(empty($appInfo)) return print(js::locate('back', 'parent'));
$this->lang->switcherMenu = $this->store->getAppViewSwitcher($appInfo);
diff --git a/frontend/module/store/view/appview.html.php b/frontend/module/store/view/appview.html.php
index 8f72abe6..0611f66d 100644
--- a/frontend/module/store/view/appview.html.php
+++ b/frontend/module/store/view/appview.html.php
@@ -24,7 +24,7 @@
alias;?>
- id}", '', true), $lang->instance->install, '', "class='iframe btn btn-primary' title='{$lang->instance->install}' data-width='500' data-app='space'");?>
+ id}", '', true), $lang->instance->install, '', "class='iframe btn btn-primary' title='{$lang->instance->install}' data-width='520' data-app='space'");?>
diff --git a/frontend/module/system/control.php b/frontend/module/system/control.php
index d0cba2c5..0a8c1229 100644
--- a/frontend/module/system/control.php
+++ b/frontend/module/system/control.php
@@ -43,14 +43,12 @@ public function dbList()
}
/**
- * Generate database auth parameters ad jump to login page.
+ * Generate database auth parameters and jump to login page.
*
- * @param istring $dbName
- * @param string $namespace
* @access public
- * @return mixed
+ * @return void
*/
- public function loginDB()
+ public function ajaxDBAuthUrl()
{
$post = fixer::input('post')
->setDefault('namespace', 'default')
diff --git a/frontend/module/system/js/dblist.js b/frontend/module/system/js/dblist.js
index e6bc1d8e..47cf7cc9 100644
--- a/frontend/module/system/js/dblist.js
+++ b/frontend/module/system/js/dblist.js
@@ -2,13 +2,11 @@ $(function()
{
$('button.db-login').on('click', function(event)
{
- console.log(event);
let dbName = $(event.target).data('db-name');
let namespace = $(event.target).data('namespace');
- $.post(createLink('system', 'logindb'), {dbName, namespace}).done(function(res)
+ $.post(createLink('system', 'ajaxDBAuthUrl'), {dbName, namespace}).done(function(res)
{
- console.log(res);
let response = JSON.parse(res);
if(response.result == 'success')
{
diff --git a/frontend/module/system/lang/zh-cn.php b/frontend/module/system/lang/zh-cn.php
index 9bc28cde..0608b07c 100644
--- a/frontend/module/system/lang/zh-cn.php
+++ b/frontend/module/system/lang/zh-cn.php
@@ -10,4 +10,4 @@
$lang->system->errors = new stdclass;
$lang->system->errors->notFoundDB = '找不到该数据库';
-$lang->system->errors->dbNameIsEmpty = '找不到该数据库';
+$lang->system->errors->dbNameIsEmpty = '数据库名为空';
diff --git a/frontend/module/upgrade/lang/version.php b/frontend/module/upgrade/lang/version.php
index 5ddfa7b3..59d26818 100644
--- a/frontend/module/upgrade/lang/version.php
+++ b/frontend/module/upgrade/lang/version.php
@@ -5,3 +5,4 @@
$lang->upgrade->fromVersions['1_1_1'] = '1.1.1';
$lang->upgrade->fromVersions['1_1_2'] = '1.1.2';
$lang->upgrade->fromVersions['1_2_0'] = '1.2.0';
+$lang->upgrade->fromVersions['1_4_0'] = '1.4.0';
diff --git a/frontend/www/index.php b/frontend/www/index.php
index c8d2ee03..8fabd55a 100644
--- a/frontend/www/index.php
+++ b/frontend/www/index.php
@@ -45,8 +45,8 @@
- 系统已更新,请登录后台运行升级指令:q manage upgrade
- The system has been updated, Please run command in console:q manage upgrade
+ 系统已更新,请登录后台运行升级指令:q upgrade quickon
+ The system has been updated, Please run command in console:q upgrade quickon
|