mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-11-28 05:00:26 +08:00
842 lines
21 KiB
Go
842 lines
21 KiB
Go
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package waveappstore
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/secretstore"
|
|
"github.com/wavetermdev/waveterm/pkg/util/fileutil"
|
|
"github.com/wavetermdev/waveterm/pkg/waveapputil"
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
)
|
|
|
|
const (
|
|
AppNSLocal = "local"
|
|
AppNSDraft = "draft"
|
|
|
|
MaxNamespaceLen = 30
|
|
MaxAppNameLen = 50
|
|
|
|
ManifestFileName = "manifest.json"
|
|
SecretBindingsFileName = "secret-bindings.json"
|
|
)
|
|
|
|
var (
|
|
namespaceRegex = regexp.MustCompile(`^@?[a-z0-9-]+$`)
|
|
appNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
|
)
|
|
|
|
type FileData struct {
|
|
Contents []byte
|
|
ModTs int64
|
|
}
|
|
|
|
func MakeAppId(appNS string, appName string) string {
|
|
return appNS + "/" + appName
|
|
}
|
|
|
|
func ParseAppId(appId string) (appNS string, appName string, err error) {
|
|
parts := strings.Split(appId, "/")
|
|
if len(parts) != 2 {
|
|
return "", "", fmt.Errorf("invalid appId format: must be namespace/name")
|
|
}
|
|
appNS = parts[0]
|
|
appName = parts[1]
|
|
if appNS == "" || appName == "" {
|
|
return "", "", fmt.Errorf("invalid appId: namespace and name cannot be empty")
|
|
}
|
|
return appNS, appName, nil
|
|
}
|
|
|
|
func ValidateAppId(appId string) error {
|
|
appNS, appName, err := ParseAppId(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(appNS) > MaxNamespaceLen {
|
|
return fmt.Errorf("namespace too long: max %d characters", MaxNamespaceLen)
|
|
}
|
|
if len(appName) > MaxAppNameLen {
|
|
return fmt.Errorf("app name too long: max %d characters", MaxAppNameLen)
|
|
}
|
|
if !namespaceRegex.MatchString(appNS) {
|
|
return fmt.Errorf("invalid namespace: must match pattern @?[a-z0-9-]+")
|
|
}
|
|
if !appNameRegex.MatchString(appName) {
|
|
return fmt.Errorf("invalid app name: must match pattern [a-zA-Z0-9_-]+")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetAppDir(appId string) (string, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return "", err
|
|
}
|
|
appNS, appName, _ := ParseAppId(appId)
|
|
homeDir := wavebase.GetHomeDir()
|
|
return filepath.Join(homeDir, "waveapps", appNS, appName), nil
|
|
}
|
|
|
|
func copyDir(src, dst string) error {
|
|
if err := os.RemoveAll(dst); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to remove existing directory: %w", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
|
return fmt.Errorf("failed to create parent directory: %w", err)
|
|
}
|
|
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relPath, err := filepath.Rel(src, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dstPath := filepath.Join(dst, relPath)
|
|
|
|
if info.IsDir() {
|
|
return os.MkdirAll(dstPath, info.Mode())
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(dstPath, data, info.Mode())
|
|
})
|
|
}
|
|
|
|
func PublishDraft(draftAppId string) (string, error) {
|
|
if err := ValidateAppId(draftAppId); err != nil {
|
|
return "", fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appNS, appName, _ := ParseAppId(draftAppId)
|
|
if appNS != AppNSDraft {
|
|
return "", fmt.Errorf("appId must be in draft namespace, got: %s", appNS)
|
|
}
|
|
|
|
draftDir, err := GetAppDir(draftAppId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := os.Stat(draftDir); os.IsNotExist(err) {
|
|
return "", fmt.Errorf("draft app does not exist: %s", draftDir)
|
|
}
|
|
|
|
localAppId := MakeAppId(AppNSLocal, appName)
|
|
localDir, err := GetAppDir(localAppId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := copyDir(draftDir, localDir); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return localAppId, nil
|
|
}
|
|
|
|
func RevertDraft(draftAppId string) error {
|
|
if err := ValidateAppId(draftAppId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appNS, appName, _ := ParseAppId(draftAppId)
|
|
if appNS != AppNSDraft {
|
|
return fmt.Errorf("appId must be in draft namespace, got: %s", appNS)
|
|
}
|
|
|
|
draftDir, err := GetAppDir(draftAppId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localAppId := MakeAppId(AppNSLocal, appName)
|
|
localDir, err := GetAppDir(localAppId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := os.Stat(localDir); os.IsNotExist(err) {
|
|
return fmt.Errorf("local app does not exist: %s", localDir)
|
|
}
|
|
|
|
return copyDir(localDir, draftDir)
|
|
}
|
|
|
|
func MakeDraftFromLocal(localAppId string) (string, error) {
|
|
if err := ValidateAppId(localAppId); err != nil {
|
|
return "", fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appNS, appName, _ := ParseAppId(localAppId)
|
|
if appNS != AppNSLocal {
|
|
return "", fmt.Errorf("appId must be in local namespace, got: %s", appNS)
|
|
}
|
|
|
|
localDir, err := GetAppDir(localAppId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := os.Stat(localDir); os.IsNotExist(err) {
|
|
return "", fmt.Errorf("local app does not exist: %s", localDir)
|
|
}
|
|
|
|
draftAppId := MakeAppId(AppNSDraft, appName)
|
|
draftDir, err := GetAppDir(draftAppId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := os.Stat(draftDir); err == nil {
|
|
// draft already exists, don't overwrite (that's what RevertDraft is for)
|
|
return draftAppId, nil
|
|
} else if !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
|
|
if err := copyDir(localDir, draftDir); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return draftAppId, nil
|
|
}
|
|
|
|
func DeleteApp(appId string) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.RemoveAll(appDir); err != nil {
|
|
return fmt.Errorf("failed to delete app directory: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateAndResolveFilePath(appDir string, fileName string) (string, error) {
|
|
if filepath.IsAbs(fileName) {
|
|
return "", fmt.Errorf("fileName must be relative, got absolute path: %s", fileName)
|
|
}
|
|
|
|
cleanPath := filepath.Clean(fileName)
|
|
if strings.HasPrefix(cleanPath, "..") || strings.Contains(cleanPath, string(filepath.Separator)+"..") {
|
|
return "", fmt.Errorf("path traversal not allowed: %s", fileName)
|
|
}
|
|
|
|
fullPath := filepath.Join(appDir, cleanPath)
|
|
resolvedPath, err := filepath.Abs(fullPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to resolve path: %w", err)
|
|
}
|
|
|
|
resolvedAppDir, err := filepath.Abs(appDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to resolve app directory: %w", err)
|
|
}
|
|
|
|
if !strings.HasPrefix(resolvedPath, resolvedAppDir+string(filepath.Separator)) && resolvedPath != resolvedAppDir {
|
|
return "", fmt.Errorf("path escapes app directory: %s", fileName)
|
|
}
|
|
|
|
return resolvedPath, nil
|
|
}
|
|
|
|
func WriteAppFile(appId string, fileName string, contents []byte) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filePath, err := validateAndResolveFilePath(appDir, fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(filePath, contents, 0644); err != nil {
|
|
return fmt.Errorf("failed to write file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ReadAppFile(appId string, fileName string) (*FileData, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return nil, fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePath, err := validateAndResolveFilePath(appDir, fileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileInfo, err := os.Stat(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to stat file: %w", err)
|
|
}
|
|
|
|
contents, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file: %w", err)
|
|
}
|
|
|
|
return &FileData{
|
|
Contents: contents,
|
|
ModTs: fileInfo.ModTime().UnixMilli(),
|
|
}, nil
|
|
}
|
|
|
|
func DeleteAppFile(appId string, fileName string) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filePath, err := validateAndResolveFilePath(appDir, fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Remove(filePath); err != nil {
|
|
return fmt.Errorf("failed to delete file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ReplaceInAppFile(appId string, fileName string, edits []fileutil.EditSpec) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filePath, err := validateAndResolveFilePath(appDir, fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fileutil.ReplaceInFile(filePath, edits)
|
|
}
|
|
|
|
func ReplaceInAppFilePartial(appId string, fileName string, edits []fileutil.EditSpec) ([]fileutil.EditResult, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return nil, fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePath, err := validateAndResolveFilePath(appDir, fileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fileutil.ReplaceInFilePartial(filePath, edits)
|
|
}
|
|
|
|
func RenameAppFile(appId string, fromFileName string, toFileName string) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fromPath, err := validateAndResolveFilePath(appDir, fromFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid source path: %w", err)
|
|
}
|
|
|
|
toPath, err := validateAndResolveFilePath(appDir, toFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid destination path: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(toPath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create destination directory: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(fromPath, toPath); err != nil {
|
|
return fmt.Errorf("failed to rename file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func FormatGoFile(appId string, fileName string) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filePath, err := validateAndResolveFilePath(appDir, fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if filepath.Ext(filePath) != ".go" {
|
|
return fmt.Errorf("file is not a Go file: %s", fileName)
|
|
}
|
|
|
|
gofmtPath, err := waveapputil.ResolveGoFmtPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve gofmt path: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command(gofmtPath, "-w", filePath)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("gofmt failed: %w\nOutput: %s", err, string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ListAllAppFiles(appId string) (*fileutil.ReadDirResult, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return nil, fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := os.Stat(appDir); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("app directory does not exist: %s", appDir)
|
|
}
|
|
|
|
return fileutil.ReadDirRecursive(appDir, 10000)
|
|
}
|
|
|
|
func ListAllApps() ([]wshrpc.AppInfo, error) {
|
|
homeDir := wavebase.GetHomeDir()
|
|
waveappsDir := filepath.Join(homeDir, "waveapps")
|
|
|
|
if _, err := os.Stat(waveappsDir); os.IsNotExist(err) {
|
|
return []wshrpc.AppInfo{}, nil
|
|
}
|
|
|
|
namespaces, err := os.ReadDir(waveappsDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read waveapps directory: %w", err)
|
|
}
|
|
|
|
var appInfos []wshrpc.AppInfo
|
|
|
|
for _, ns := range namespaces {
|
|
if !ns.IsDir() {
|
|
continue
|
|
}
|
|
|
|
namespace := ns.Name()
|
|
nsPath := filepath.Join(waveappsDir, namespace)
|
|
|
|
apps, err := os.ReadDir(nsPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, app := range apps {
|
|
if !app.IsDir() {
|
|
continue
|
|
}
|
|
|
|
appName := app.Name()
|
|
appId := MakeAppId(namespace, appName)
|
|
|
|
if err := ValidateAppId(appId); err == nil {
|
|
modTime, _ := GetAppModTime(appId)
|
|
appInfo := wshrpc.AppInfo{
|
|
AppId: appId,
|
|
ModTime: modTime,
|
|
}
|
|
|
|
if manifest, err := ReadAppManifest(appId); err == nil {
|
|
appInfo.Manifest = manifest
|
|
}
|
|
|
|
appInfos = append(appInfos, appInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
return appInfos, nil
|
|
}
|
|
|
|
func GetAppModTime(appId string) (int64, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
homeDir := wavebase.GetHomeDir()
|
|
appNS, appName, err := ParseAppId(appId)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
appPath := filepath.Join(homeDir, "waveapps", appNS, appName)
|
|
appGoPath := filepath.Join(appPath, "app.go")
|
|
|
|
fileInfo, err := os.Stat(appGoPath)
|
|
if err == nil {
|
|
return fileInfo.ModTime().UnixMilli(), nil
|
|
}
|
|
|
|
dirInfo, err := os.Stat(appPath)
|
|
if err != nil {
|
|
return 0, nil
|
|
}
|
|
|
|
return dirInfo.ModTime().UnixMilli(), nil
|
|
}
|
|
|
|
func ListAllEditableApps() ([]wshrpc.AppInfo, error) {
|
|
homeDir := wavebase.GetHomeDir()
|
|
waveappsDir := filepath.Join(homeDir, "waveapps")
|
|
|
|
if _, err := os.Stat(waveappsDir); os.IsNotExist(err) {
|
|
return []wshrpc.AppInfo{}, nil
|
|
}
|
|
|
|
localApps := make(map[string]bool)
|
|
draftApps := make(map[string]bool)
|
|
|
|
localPath := filepath.Join(waveappsDir, AppNSLocal)
|
|
if localEntries, err := os.ReadDir(localPath); err == nil {
|
|
for _, app := range localEntries {
|
|
if app.IsDir() {
|
|
appName := app.Name()
|
|
appId := MakeAppId(AppNSLocal, appName)
|
|
if err := ValidateAppId(appId); err == nil {
|
|
localApps[appName] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
draftPath := filepath.Join(waveappsDir, AppNSDraft)
|
|
if draftEntries, err := os.ReadDir(draftPath); err == nil {
|
|
for _, app := range draftEntries {
|
|
if app.IsDir() {
|
|
appName := app.Name()
|
|
appId := MakeAppId(AppNSDraft, appName)
|
|
if err := ValidateAppId(appId); err == nil {
|
|
draftApps[appName] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
allAppNames := make(map[string]bool)
|
|
for appName := range localApps {
|
|
allAppNames[appName] = true
|
|
}
|
|
for appName := range draftApps {
|
|
allAppNames[appName] = true
|
|
}
|
|
|
|
var appInfos []wshrpc.AppInfo
|
|
for appName := range allAppNames {
|
|
var appId string
|
|
var manifestAppId string
|
|
if localApps[appName] {
|
|
appId = MakeAppId(AppNSLocal, appName)
|
|
} else {
|
|
appId = MakeAppId(AppNSDraft, appName)
|
|
}
|
|
|
|
if draftApps[appName] {
|
|
manifestAppId = MakeAppId(AppNSDraft, appName)
|
|
} else {
|
|
manifestAppId = appId
|
|
}
|
|
|
|
modTime, _ := GetAppModTime(manifestAppId)
|
|
|
|
appInfo := wshrpc.AppInfo{
|
|
AppId: appId,
|
|
ModTime: modTime,
|
|
}
|
|
|
|
if manifest, err := ReadAppManifest(manifestAppId); err == nil {
|
|
appInfo.Manifest = manifest
|
|
}
|
|
|
|
appInfos = append(appInfos, appInfo)
|
|
}
|
|
|
|
return appInfos, nil
|
|
}
|
|
|
|
func DraftHasLocalVersion(draftAppId string) (bool, error) {
|
|
if err := ValidateAppId(draftAppId); err != nil {
|
|
return false, fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appNS, appName, _ := ParseAppId(draftAppId)
|
|
if appNS != AppNSDraft {
|
|
return false, fmt.Errorf("appId must be in draft namespace, got: %s", appNS)
|
|
}
|
|
|
|
localAppId := MakeAppId(AppNSLocal, appName)
|
|
localDir, err := GetAppDir(localAppId)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if _, err := os.Stat(localDir); os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// RenameLocalApp renames a local app by renaming its directories in both the local and draft namespaces.
|
|
// It takes the current app name and the new app name (without namespace prefixes).
|
|
// Both local/[appName] and draft/[appName] will be renamed if they exist.
|
|
// Returns an error if the app doesn't exist in either namespace, if the new name is invalid,
|
|
// or if the new name conflicts with an existing app.
|
|
func RenameLocalApp(appName string, newAppName string) error {
|
|
// Validate the old app name by constructing a valid appId
|
|
oldLocalAppId := MakeAppId(AppNSLocal, appName)
|
|
if err := ValidateAppId(oldLocalAppId); err != nil {
|
|
return fmt.Errorf("invalid app name: %w", err)
|
|
}
|
|
|
|
// Validate the new app name by constructing a valid appId
|
|
newLocalAppId := MakeAppId(AppNSLocal, newAppName)
|
|
if err := ValidateAppId(newLocalAppId); err != nil {
|
|
return fmt.Errorf("invalid new app name: %w", err)
|
|
}
|
|
|
|
homeDir := wavebase.GetHomeDir()
|
|
waveappsDir := filepath.Join(homeDir, "waveapps")
|
|
|
|
oldLocalDir := filepath.Join(waveappsDir, AppNSLocal, appName)
|
|
newLocalDir := filepath.Join(waveappsDir, AppNSLocal, newAppName)
|
|
oldDraftDir := filepath.Join(waveappsDir, AppNSDraft, appName)
|
|
newDraftDir := filepath.Join(waveappsDir, AppNSDraft, newAppName)
|
|
|
|
// Check if at least one of the apps exists
|
|
localExists := false
|
|
draftExists := false
|
|
if _, err := os.Stat(oldLocalDir); err == nil {
|
|
localExists = true
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to check local app: %w", err)
|
|
}
|
|
|
|
if _, err := os.Stat(oldDraftDir); err == nil {
|
|
draftExists = true
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to check draft app: %w", err)
|
|
}
|
|
|
|
if !localExists && !draftExists {
|
|
return fmt.Errorf("app '%s' does not exist in local or draft namespace", appName)
|
|
}
|
|
|
|
// Check if new app name already exists in either namespace
|
|
if _, err := os.Stat(newLocalDir); err == nil {
|
|
return fmt.Errorf("local app '%s' already exists", newAppName)
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to check if new local app exists: %w", err)
|
|
}
|
|
|
|
if _, err := os.Stat(newDraftDir); err == nil {
|
|
return fmt.Errorf("draft app '%s' already exists", newAppName)
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to check if new draft app exists: %w", err)
|
|
}
|
|
|
|
// Rename local app if it exists
|
|
if localExists {
|
|
if err := os.Rename(oldLocalDir, newLocalDir); err != nil {
|
|
return fmt.Errorf("failed to rename local app: %w", err)
|
|
}
|
|
}
|
|
|
|
// Rename draft app if it exists
|
|
if draftExists {
|
|
if err := os.Rename(oldDraftDir, newDraftDir); err != nil {
|
|
// If local was renamed but draft fails, try to rollback local rename
|
|
if localExists {
|
|
if rollbackErr := os.Rename(newLocalDir, oldLocalDir); rollbackErr != nil {
|
|
return fmt.Errorf("failed to rename draft app (and failed to rollback local rename: %v): %w", rollbackErr, err)
|
|
}
|
|
}
|
|
return fmt.Errorf("failed to rename draft app: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ReadAppManifest(appId string) (*wshrpc.AppManifest, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return nil, fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
manifestPath := filepath.Join(appDir, ManifestFileName)
|
|
data, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read %s: %w", ManifestFileName, err)
|
|
}
|
|
|
|
var manifest wshrpc.AppManifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
return nil, fmt.Errorf("failed to parse %s: %w", ManifestFileName, err)
|
|
}
|
|
|
|
return &manifest, nil
|
|
}
|
|
|
|
func ReadAppSecretBindings(appId string) (map[string]string, error) {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return nil, fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bindingsPath := filepath.Join(appDir, SecretBindingsFileName)
|
|
data, err := os.ReadFile(bindingsPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return make(map[string]string), nil
|
|
}
|
|
return nil, fmt.Errorf("failed to read %s: %w", SecretBindingsFileName, err)
|
|
}
|
|
|
|
var bindings map[string]string
|
|
if err := json.Unmarshal(data, &bindings); err != nil {
|
|
return nil, fmt.Errorf("failed to parse %s: %w", SecretBindingsFileName, err)
|
|
}
|
|
|
|
if bindings == nil {
|
|
bindings = make(map[string]string)
|
|
}
|
|
|
|
return bindings, nil
|
|
}
|
|
|
|
func WriteAppSecretBindings(appId string, bindings map[string]string) error {
|
|
if err := ValidateAppId(appId); err != nil {
|
|
return fmt.Errorf("invalid appId: %w", err)
|
|
}
|
|
|
|
appDir, err := GetAppDir(appId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if bindings == nil {
|
|
bindings = make(map[string]string)
|
|
}
|
|
|
|
data, err := json.MarshalIndent(bindings, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal bindings: %w", err)
|
|
}
|
|
|
|
bindingsPath := filepath.Join(appDir, SecretBindingsFileName)
|
|
if err := os.WriteFile(bindingsPath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write %s: %w", SecretBindingsFileName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func BuildAppSecretEnv(appId string, manifest *wshrpc.AppManifest, bindings map[string]string) (map[string]string, error) {
|
|
if manifest == nil {
|
|
return make(map[string]string), nil
|
|
}
|
|
|
|
if bindings == nil {
|
|
bindings = make(map[string]string)
|
|
}
|
|
|
|
secretEnv := make(map[string]string)
|
|
|
|
for secretName, secretMeta := range manifest.Secrets {
|
|
boundSecretName, hasBinding := bindings[secretName]
|
|
|
|
if !secretMeta.Optional && !hasBinding {
|
|
return nil, fmt.Errorf("required secret %q is not bound", secretName)
|
|
}
|
|
|
|
if !hasBinding {
|
|
continue
|
|
}
|
|
|
|
secretValue, exists, err := secretstore.GetSecret(boundSecretName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get secret %q: %w", boundSecretName, err)
|
|
}
|
|
|
|
if !exists {
|
|
if !secretMeta.Optional {
|
|
return nil, fmt.Errorf("required secret %q is bound to %q which does not exist in secret store", secretName, boundSecretName)
|
|
}
|
|
continue
|
|
}
|
|
|
|
secretEnv[secretName] = secretValue
|
|
}
|
|
|
|
return secretEnv, nil
|
|
}
|