Compare commits

...

8 Commits

9 changed files with 222 additions and 43 deletions

View File

@@ -170,7 +170,7 @@ spec:
priorityClassName: "system-cluster-critical"
containers:
- name: app-service
image: beclab/app-service:0.4.79
image: beclab/app-service:0.5.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6755

View File

@@ -40,7 +40,7 @@ type UserEnvSyncController struct {
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
//+kubebuilder:rbac:groups=iam.kubesphere.io,resources=users,verbs=get;list;watch
//+kubebuilder:rbac:groups=sys.bytetrade.io,resources=userenvs,verbs=get;list;watch;create
//+kubebuilder:rbac:groups=sys.bytetrade.io,resources=userenvs,verbs=get;list;watch;create;patch;update
func (r *UserEnvSyncController) SetupWithManager(mgr ctrl.Manager) error {
cmPred := predicate.NewPredicateFuncs(func(obj client.Object) bool {
@@ -164,14 +164,63 @@ func (r *UserEnvSyncController) syncUserEnvForUser(ctx context.Context, username
return 0, fmt.Errorf("list userenvs in %s failed: %w", userNs, err)
}
existSet := make(map[string]struct{}, len(existing.Items))
existByName := make(map[string]*sysv1alpha1.UserEnv, len(existing.Items))
for i := range existing.Items {
existSet[existing.Items[i].EnvName] = struct{}{}
existByName[existing.Items[i].EnvName] = &existing.Items[i]
}
created := 0
for _, spec := range base {
if _, ok := existSet[spec.EnvName]; ok {
if ue, ok := existByName[spec.EnvName]; ok {
original := ue.DeepCopy()
updated := false
if ue.Default == "" && spec.Default != "" {
ue.Default = spec.Default
updated = true
}
if ue.Type == "" && spec.Type != "" {
ue.Type = spec.Type
updated = true
}
if ue.Title == "" && spec.Title != "" {
ue.Title = spec.Title
updated = true
}
if ue.Description == "" && spec.Description != "" {
ue.Description = spec.Description
updated = true
}
if ue.RemoteOptions == "" && spec.RemoteOptions != "" {
ue.RemoteOptions = spec.RemoteOptions
updated = true
}
if ue.Regex == "" && spec.Regex != "" {
ue.Regex = spec.Regex
updated = true
}
if len(spec.Options) > 0 {
existOpt := make(map[string]struct{}, len(ue.Options))
for _, it := range ue.Options {
existOpt[it.Value] = struct{}{}
}
for _, it := range spec.Options {
if _, exists := existOpt[it.Value]; exists {
continue
}
ue.Options = append(ue.Options, it)
existOpt[it.Value] = struct{}{}
updated = true
}
}
if updated {
if err := r.Patch(ctx, ue, client.MergeFrom(original)); err != nil {
return created, fmt.Errorf("patch userenv %s/%s failed: %w", ue.Namespace, ue.Name, err)
}
klog.Infof("UserEnvSync: patched userenv %s/%s for user %s", ue.Namespace, ue.Name, username)
}
continue
}
name, err := apputils.EnvNameToResourceName(spec.EnvName)

View File

@@ -13,6 +13,7 @@ const (
AppMarketSourceKey = constants.AppMarketSourceKey
AppInstallSourceKey = "bytetrade.io/install-source"
AppUninstallAllKey = "bytetrade.io/uninstall-all"
AppDeleteDataKey = "bytetrade.io/delete-data"
AppStopAllKey = "bytetrade.io/stop-all"
AppResumeAllKey = "bytetrade.io/resume-all"
AppImagesKey = "bytetrade.io/images"
@@ -145,7 +146,8 @@ type Image struct {
// UninstallRequest represents a request to uninstall an application.
type UninstallRequest struct {
All bool `json:"all"`
All bool `json:"all"`
DeleteData bool `json:"deleteData"`
}
// StopRequest represents a request to stop an application.

View File

@@ -6,10 +6,12 @@ import (
"io"
"net/http"
"net/url"
"sync"
"time"
sysv1alpha1 "github.com/beclab/Olares/framework/app-service/api/sys.bytetrade.io/v1alpha1"
"github.com/beclab/Olares/framework/app-service/pkg/apiserver/api"
"github.com/beclab/Olares/framework/app-service/pkg/constants"
"github.com/beclab/Olares/framework/app-service/pkg/utils"
apputils "github.com/beclab/Olares/framework/app-service/pkg/utils/app"
"github.com/emicklei/go-restful/v3"
@@ -76,32 +78,74 @@ func (h *Handler) updateAppEnv(req *restful.Request, resp *restful.Response) {
return
}
var refEnvOnce sync.Once
var listErr error
refEnvs := make(map[string]string)
updated := false
original := targetAppEnv.DeepCopy()
for i, existingEnv := range targetAppEnv.Envs {
for _, env := range updatedEnvs {
if existingEnv.EnvName == env.EnvName {
if !existingEnv.Editable {
api.HandleBadRequest(resp, req, fmt.Errorf("app env '%s' is not editable", env.EnvName))
return
}
if existingEnv.Required && existingEnv.Default == "" && env.Value == "" {
api.HandleBadRequest(resp, req, fmt.Errorf("app env '%s' is required", env.EnvName))
return
}
if existingEnv.Value != env.Value {
if err := existingEnv.ValidateValue(env.Value); err != nil {
api.HandleBadRequest(resp, req, fmt.Errorf("failed to update app env '%s': %v", env.EnvName, err))
if existingEnv.EnvName != env.EnvName {
continue
}
if !existingEnv.Editable {
api.HandleBadRequest(resp, req, fmt.Errorf("app env '%s' is not editable", env.EnvName))
return
}
if existingEnv.Required && existingEnv.Default == "" && env.Value == "" && (env.ValueFrom == nil || env.ValueFrom.EnvName == "") {
api.HandleBadRequest(resp, req, fmt.Errorf("app env '%s' is required", env.EnvName))
return
}
if env.ValueFrom != nil && env.ValueFrom.EnvName != "" && (existingEnv.ValueFrom == nil || existingEnv.ValueFrom.EnvName != env.ValueFrom.EnvName) {
refEnvOnce.Do(func() {
sysenvs := new(sysv1alpha1.SystemEnvList)
listErr = h.ctrlClient.List(req.Request.Context(), sysenvs)
if listErr != nil {
return
}
targetAppEnv.Envs[i].Value = env.Value
updated = true
if existingEnv.ApplyOnChange {
targetAppEnv.NeedApply = true
userenvs := new(sysv1alpha1.UserEnvList)
listErr = h.ctrlClient.List(req.Request.Context(), userenvs, client.InNamespace(utils.UserspaceName(owner)))
for _, sysenv := range sysenvs.Items {
refEnvs[sysenv.EnvName] = sysenv.GetEffectiveValue()
}
for _, userenv := range userenvs.Items {
refEnvs[userenv.EnvName] = userenv.GetEffectiveValue()
}
})
if listErr != nil {
api.HandleInternalError(resp, req, fmt.Errorf("failed to list referenced envs: %s", listErr))
return
}
break
value, ok := refEnvs[env.ValueFrom.EnvName]
if !ok {
api.HandleBadRequest(resp, req, fmt.Errorf("app env '%s' references unknown env '%s'", env.EnvName, env.ValueFrom.EnvName))
return
}
if existingEnv.Required && value == "" {
api.HandleBadRequest(resp, req, fmt.Errorf("required app env '%s' references empty env '%s'", env.EnvName, env.ValueFrom.EnvName))
return
}
if existingEnv.ValidateValue(value) != nil {
api.HandleBadRequest(resp, req, fmt.Errorf("app env '%s' references invalid value '%s' from '%s': %v", env.EnvName, value, env.ValueFrom.EnvName, err))
return
}
targetAppEnv.Envs[i].ValueFrom = env.ValueFrom
targetAppEnv.Envs[i].Value = value
targetAppEnv.Envs[i].ValueFrom.Status = constants.EnvRefStatusSynced
updated = true
} else if existingEnv.Value != env.Value {
if err := existingEnv.ValidateValue(env.Value); err != nil {
api.HandleBadRequest(resp, req, fmt.Errorf("failed to update app env '%s': %v", env.EnvName, err))
return
}
targetAppEnv.Envs[i].Value = env.Value
updated = true
}
if updated && existingEnv.ApplyOnChange {
targetAppEnv.NeedApply = true
}
break
}
}

View File

@@ -77,6 +77,7 @@ func (h *Handler) uninstall(req *restful.Request, resp *restful.Response) {
}
am.Annotations[api.AppTokenKey] = token
am.Annotations[api.AppUninstallAllKey] = fmt.Sprintf("%t", request.All)
am.Annotations[api.AppDeleteDataKey] = fmt.Sprintf("%t", request.DeleteData)
err = h.ctrlClient.Update(req.Request.Context(), &am)
if err != nil {
api.HandleError(resp, req, err)

View File

@@ -586,6 +586,18 @@ func (h *Handler) getApplicationPermission(req *restful.Request, resp *restful.R
return
}
// sys app does not have app config
if am.Spec.Config == "" {
ret := &applicationPermission{
App: am.Spec.AppName,
Owner: owner,
Permissions: []permission{},
}
resp.WriteAsJson(ret)
return
}
var appConfig appcfg.ApplicationConfig
err = am.GetAppConfig(&appConfig)
if err != nil {

View File

@@ -29,8 +29,14 @@ func (h *HelmOps) UninstallAll() error {
if err != nil {
return err
}
appName := fmt.Sprintf("%s-%s", h.app.Namespace, h.app.AppName)
appmgr, err := h.client.AppClient.AppV1alpha1().ApplicationManagers().Get(h.ctx, appName, metav1.GetOptions{})
if err != nil {
return err
}
deleteData := appmgr.Annotations["bytetrade.io/delete-data"] == "true"
appCacheDirs, err := apputils.TryToGetAppdataDirFromDeployment(h.ctx, h.app.Namespace, h.app.AppName, h.app.OwnerName)
appCacheDirs, appDataDirs, err := apputils.TryToGetAppdataDirFromDeployment(h.ctx, h.app.Namespace, h.app.AppName, h.app.OwnerName, deleteData)
if err != nil {
klog.Warningf("get app %s cache dir failed %v", h.app.AppName, err)
}
@@ -48,6 +54,13 @@ func (h *HelmOps) UninstallAll() error {
klog.Errorf("Failed to clear app cache dirs %v err=%v", appCacheDirs, err)
return err
}
if deleteData {
h.ClearData(client, appDataDirs)
if err != nil {
klog.Errorf("Failed to clear app data dirs %v err=%v", appDataDirs, err)
return err
}
}
err = h.DeleteNamespace(client, h.app.Namespace)
if err != nil {
@@ -117,7 +130,7 @@ func (h *HelmOps) ClearCache(client kubernetes.Interface, appCacheDirs []string)
formattedAppCacheDirs := apputils.FormatCacheDirs(appCacheDirs)
for _, n := range nodes.Items {
URL := fmt.Sprintf(constants.AppDataDirURL, n.Name)
URL := fmt.Sprintf(constants.AppCacheDirURL, n.Name)
c.SetHeader("X-Terminus-Node", n.Name)
c.SetHeader("X-Bfl-User", h.app.OwnerName)
res, e := c.R().SetBody(map[string]interface{}{
@@ -137,6 +150,32 @@ func (h *HelmOps) ClearCache(client kubernetes.Interface, appCacheDirs []string)
return nil
}
func (h *HelmOps) ClearData(client kubernetes.Interface, appDataDirs []string) error {
if len(appDataDirs) > 0 {
klog.Infof("clear app data dirs: %v", appDataDirs)
c := resty.New().SetTimeout(2 * time.Second).
SetAuthToken(h.token)
formattedAppDataDirs := apputils.FormatCacheDirs(appDataDirs)
URL := constants.AppDataDirURL
c.SetHeader("X-Bfl-User", h.app.OwnerName)
res, e := c.R().SetBody(map[string]interface{}{
"dirents": formattedAppDataDirs,
}).Delete(URL)
if e != nil {
klog.Errorf("Failed to delete data dir err=%v", e)
return nil
}
if res.StatusCode() != http.StatusOK {
klog.Infof("delete app data failed with: %v", res.String())
}
}
return nil
}
func (h *HelmOps) ClearMiddlewareRequests(middlewareNamespace string) {
// delete middleware requests crd
for _, mt := range middlewareTypes {

View File

@@ -93,7 +93,8 @@ const (
DependencyTypeSystem = "system"
DependencyTypeApp = "application"
AppDataDirURL = "http://files-service.os-framework/api/resources/cache/%s/"
AppCacheDirURL = "http://files-service.os-framework/api/resources/cache/%s/"
AppDataDirURL = "http://files-service.os-framework/api/resources/drive/Data/"
UserSpaceDirKey = "userspace_hostpath"
UserAppDataDirKey = "appcache_hostpath"

View File

@@ -22,7 +22,6 @@ import (
"github.com/beclab/Olares/framework/app-service/api/app.bytetrade.io/v1alpha1"
"github.com/beclab/Olares/framework/app-service/pkg/appcfg"
"github.com/beclab/Olares/framework/app-service/pkg/constants"
"github.com/beclab/Olares/framework/app-service/pkg/generated/clientset/versioned"
"github.com/beclab/Olares/framework/app-service/pkg/users/userspace"
"github.com/beclab/Olares/framework/app-service/pkg/utils"
"github.com/beclab/Olares/framework/app-service/pkg/utils/files"
@@ -554,7 +553,7 @@ func parseDestination(dest string) (string, string, error) {
return alias, tokens[len(tokens)-1], nil
}
func TryToGetAppdataDirFromDeployment(ctx context.Context, namespace, name, owner string) (appdirs []string, err error) {
func TryToGetAppdataDirFromDeployment(ctx context.Context, namespace, name, owner string, appData bool) (appCacheDirs []string, appDataDirs []string, err error) {
userspaceNs := utils.UserspaceName(owner)
config, err := ctrl.GetConfig()
if err != nil {
@@ -568,7 +567,6 @@ func TryToGetAppdataDirFromDeployment(ctx context.Context, namespace, name, owne
if err != nil {
return
}
appName := fmt.Sprintf("%s-%s", namespace, name)
appCachePath := sts.GetAnnotations()["appcache_hostpath"]
if len(appCachePath) == 0 {
err = errors.New("empty appcache_hostpath")
@@ -577,20 +575,23 @@ func TryToGetAppdataDirFromDeployment(ctx context.Context, namespace, name, owne
if !strings.HasSuffix(appCachePath, "/") {
appCachePath += "/"
}
dClient, err := versioned.NewForConfig(config)
if err != nil {
userspacePath := sts.GetAnnotations()["userspace_hostpath"]
if len(userspacePath) == 0 {
err = errors.New("empty userspace_hostpath annotation")
return
}
appCRD, err := dClient.AppV1alpha1().Applications().Get(ctx, appName, metav1.GetOptions{})
if err != nil {
return
appDataPath := filepath.Join(userspacePath, "Data")
if !strings.HasSuffix(appDataPath, "/") {
appDataPath += "/"
}
deploymentName := appCRD.Spec.DeploymentName
deploymentName := name
deployment, err := clientset.AppsV1().Deployments(namespace).
Get(context.Background(), deploymentName, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return tryToGetAppdataDirFromSts(ctx, namespace, deploymentName, appCachePath)
return tryToGetAppdataDirFromSts(ctx, namespace, deploymentName, appCachePath, appDataPath)
}
return
}
@@ -602,15 +603,31 @@ func TryToGetAppdataDirFromDeployment(ctx context.Context, namespace, name, owne
if appDirSet.Has(appDir) {
continue
}
appdirs = append(appdirs, appDir)
appCacheDirs = append(appCacheDirs, appDir)
appDirSet.Insert(appDir)
}
}
}
return appdirs, nil
if appData {
appDirSet := sets.NewString()
for _, v := range deployment.Spec.Template.Spec.Volumes {
if v.HostPath != nil && strings.HasPrefix(v.HostPath.Path, appDataPath) && len(v.HostPath.Path) > len(appDataPath) {
appDir := GetFirstSubDir(v.HostPath.Path, appDataPath)
if appDir != "" {
if appDirSet.Has(appDir) {
continue
}
appDataDirs = append(appDataDirs, appDir)
appDirSet.Insert(appDir)
}
}
}
}
return appCacheDirs, appDataDirs, nil
}
func tryToGetAppdataDirFromSts(ctx context.Context, namespace, stsName, baseDir string) (appdirs []string, err error) {
func tryToGetAppdataDirFromSts(ctx context.Context, namespace, stsName, appCacheDir, appDataDir string) (appCacheDirs []string, appDataDirs []string, err error) {
config, err := ctrl.GetConfig()
if err != nil {
return
@@ -627,18 +644,32 @@ func tryToGetAppdataDirFromSts(ctx context.Context, namespace, stsName, baseDir
}
appDirSet := sets.NewString()
for _, v := range sts.Spec.Template.Spec.Volumes {
if v.HostPath != nil && strings.HasPrefix(v.HostPath.Path, baseDir) && len(v.HostPath.Path) > len(baseDir) {
appDir := GetFirstSubDir(v.HostPath.Path, baseDir)
if v.HostPath != nil && strings.HasPrefix(v.HostPath.Path, appCacheDir) && len(v.HostPath.Path) > len(appCacheDir) {
appDir := GetFirstSubDir(v.HostPath.Path, appCacheDir)
if appDir != "" {
if appDirSet.Has(appDir) {
continue
}
appdirs = append(appdirs, appDir)
appCacheDirs = append(appCacheDirs, appDir)
appDirSet.Insert(appDir)
}
}
}
return appdirs, nil
appDirSet = sets.NewString()
for _, v := range sts.Spec.Template.Spec.Volumes {
if v.HostPath != nil && strings.HasPrefix(v.HostPath.Path, appDataDir) && len(v.HostPath.Path) > len(appDataDir) {
appDir := GetFirstSubDir(v.HostPath.Path, appDataDir)
if appDir != "" {
if appDirSet.Has(appDir) {
continue
}
appDataDirs = append(appDataDirs, appDir)
appDirSet.Insert(appDir)
}
}
}
return appCacheDirs, appDataDirs, nil
}
func GetFirstSubDir(fullPath, basePath string) string {