Skip to content

Commit

Permalink
Added app level config update apis
Browse files Browse the repository at this point in the history
  • Loading branch information
akclace committed Aug 23, 2024
1 parent 5db6df0 commit f774465
Show file tree
Hide file tree
Showing 16 changed files with 243 additions and 91 deletions.
22 changes: 11 additions & 11 deletions cmd/clace/app_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,21 @@ func appCreateCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig)
})
flags = append(flags,
&cli.StringSliceFlag{
Name: "container-options",
Name: "container-option",
Aliases: []string{"copt"},
Usage: "Set a container option. Format is opt[=optValue]",
})
flags = append(flags,
&cli.StringSliceFlag{
Name: "container-args",
Name: "container-arg",
Aliases: []string{"carg"},
Usage: "Set an argument for building the container image. Format is argKey=argValue",
})

flags = append(flags,
&cli.StringSliceFlag{
Name: "app-config",
Aliases: []string{"config"},
Aliases: []string{"conf"},
Usage: "Set an default config option for the app. Format is configKey=configValue",
})

Expand Down Expand Up @@ -133,14 +133,14 @@ Examples:
paramValues[key] = value
}

containerOptions := cCtx.StringSlice("container-options")
containerOptions := cCtx.StringSlice("container-option")
coptMap := make(map[string]string)
for _, param := range containerOptions {
key, value, _ := strings.Cut(param, "=")
coptMap[key] = value // value can be empty string
}

containerArgs := cCtx.StringSlice("container-args")
containerArgs := cCtx.StringSlice("container-arg")
cargMap := make(map[string]string)
for _, param := range containerArgs {
key, value, ok := strings.Cut(param, "=")
Expand All @@ -150,14 +150,14 @@ Examples:
cargMap[key] = value
}

appDefaults := cCtx.StringSlice("app-config")
defMap := make(map[string]string)
for _, def := range appDefaults {
appConfig := cCtx.StringSlice("app-config")
confMap := make(map[string]string)
for _, def := range appConfig {
key, value, ok := strings.Cut(def, "=")
if !ok {
return fmt.Errorf("invalid app default format: %s", def)
return fmt.Errorf("invalid app config format: %s", def)
}
defMap[key] = value
confMap[key] = value
}

body := types.CreateAppRequest{
Expand All @@ -171,7 +171,7 @@ Examples:
ParamValues: paramValues,
ContainerOptions: coptMap,
ContainerArgs: cargMap,
AppDefaults: defMap,
AppConfig: confMap,
}
var createResult types.AppCreateResponse
err := client.Post("/_clace/app", values, body, &createResult)
Expand Down
78 changes: 75 additions & 3 deletions cmd/clace/app_updates.go → cmd/clace/app_update_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ func appUpdateMetadataCommand(commonFlags []cli.Flag, clientConfig *types.Client
Usage: `Update Clace app metadata. Metadata updates are staged and have to be promoted to prod. Use "clace param" to update app parameter metadata.`,
Subcommands: []*cli.Command{
appUpdateAppSpec(commonFlags, clientConfig),
appUpdateConfig(commonFlags, clientConfig, "container-option", "copt", types.AppMetadataContainerOptions),
appUpdateConfig(commonFlags, clientConfig, "container-arg", "carg", types.AppMetadataContainerArgs),
appUpdateConfig(commonFlags, clientConfig, "app-config", "conf", types.AppMetadataAppConfig),
},
}
}
Expand All @@ -284,7 +287,7 @@ func appUpdateAppSpec(commonFlags []cli.Flag, clientConfig *types.ClientConfig)
UsageText: `args: <value:spec_name|none> <appPathGlob>
The first required argument <value> is a string, a valid app spec name or - (to unset spec).
The second required argument is <appPathGlob>. ` + PATH_SPEC_HELP + `
The last required argument is <appPathGlob>. ` + PATH_SPEC_HELP + `
Examples:
Update all apps, across domains: clace app update-metadata spec - all
Expand All @@ -310,9 +313,78 @@ The second required argument is <appPathGlob>. ` + PATH_SPEC_HELP + `
}

for _, updateResult := range updateResponse.StagedUpdateResults {
fmt.Printf("Updating %s\n", updateResult)
fmt.Printf("Updated %s\n", updateResult)
}

if len(updateResponse.PromoteResults) > 0 {
fmt.Fprintf(cCtx.App.Writer, "Promoted apps: ")
for i, promoteResult := range updateResponse.PromoteResults {
if i > 0 {
fmt.Fprintf(cCtx.App.Writer, ", ")
}
fmt.Fprintf(cCtx.App.Writer, "%s", promoteResult)
}
fmt.Fprintln(cCtx.App.Writer)
}

fmt.Fprintf(cCtx.App.Writer, "%d app(s) updated, %d app(s) promoted.\n", len(updateResponse.StagedUpdateResults), len(updateResponse.PromoteResults))

if updateResponse.DryRun {
fmt.Print(DRY_RUN_MESSAGE)
}

return nil
},
}
}

// appUpdateConfig creates a command to update app metadata config
func appUpdateConfig(commonFlags []cli.Flag, clientConfig *types.ClientConfig, arg string, shortFlag string, configType types.AppMetadataConfigType) *cli.Command {
flags := make([]cli.Flag, 0, len(commonFlags)+2)
flags = append(flags, commonFlags...)
flags = append(flags, dryRunFlag())
flags = append(flags, newBoolFlag(PROMOTE_FLAG, "p", "Promote the change from stage to prod", false))

return &cli.Command{
Name: arg,
Aliases: []string{shortFlag},
Usage: fmt.Sprintf("Update %s metadata for apps", arg),
Flags: flags,
Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewTomlSourceFromFlagFunc(configFileFlagName)),
ArgsUsage: "key=value <appPathGlob>",

UsageText: fmt.Sprintf(`args: key=value [key=value ...] <appPathGlob>
The initial arguments key=value are strings, the key to set and the value to use delimited by =. The value is optional for
container options. The last argument is <appPathGlob>. `+PATH_SPEC_HELP+`
Examples:
Update all apps, across domains: clace app update-metadata %s key=value all
Update apps in the example.com domain: clace app update-metadata %s key=value "example.com:**"`, arg, arg),

Action: func(cCtx *cli.Context) error {
if cCtx.NArg() < 2 {
return fmt.Errorf("requires at least two arguments: key=value [key=value ...] <appPathGlob>")
}

client := system.NewHttpClient(clientConfig.ServerUri, clientConfig.AdminUser, clientConfig.Client.AdminPassword, clientConfig.Client.SkipCertCheck)
values := url.Values{}

values.Add("appPathGlob", cCtx.Args().Get(cCtx.NArg()-1))
values.Add(DRY_RUN_ARG, strconv.FormatBool(cCtx.Bool(DRY_RUN_FLAG)))
values.Add(PROMOTE_ARG, strconv.FormatBool(cCtx.Bool(PROMOTE_FLAG)))

body := types.CreateUpdateAppMetadataRequest()
body.ConfigType = configType
body.ConfigEntries = cCtx.Args().Slice()[:cCtx.NArg()-1]

var updateResponse types.AppUpdateMetadataResponse
if err := client.Post("/_clace/app_metadata", values, body, &updateResponse); err != nil {
return err
}

for _, updateResult := range updateResponse.StagedUpdateResults {
fmt.Printf("Updated %s\n", updateResult)
}
fmt.Fprintf(cCtx.App.Writer, "%d app(s) updated.\n", len(updateResponse.StagedUpdateResults))

if len(updateResponse.PromoteResults) > 0 {
fmt.Fprintf(cCtx.App.Writer, "Promoted apps: ")
Expand Down
36 changes: 18 additions & 18 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type App struct {
Name string
CustomLayout bool

Config *apptype.AppConfig
codeConfig *apptype.CodeConfig
sourceFS *appfs.SourceFs
initMutex sync.Mutex
initialized bool
Expand All @@ -73,7 +73,7 @@ type App struct {
sseListeners []chan SSEMessage
funcMap template.FuncMap
starlarkCache map[string]*starlarkCacheEntry
appDefaults types.AppDefaults
appConfig types.AppConfig
}

type starlarkCacheEntry struct {
Expand All @@ -88,7 +88,7 @@ type SSEMessage struct {

func NewApp(sourceFS *appfs.SourceFs, workFS *appfs.WorkFs, logger *types.Logger,
appEntry *types.AppEntry, systemConfig *types.SystemConfig,
plugins map[string]types.PluginSettings, appDefaults types.AppDefaults) (*App, error) {
plugins map[string]types.PluginSettings, appConfig types.AppConfig) (*App, error) {
newApp := &App{
sourceFS: sourceFS,
Logger: logger,
Expand All @@ -97,8 +97,8 @@ func NewApp(sourceFS *appfs.SourceFs, workFS *appfs.WorkFs, logger *types.Logger
starlarkCache: map[string]*starlarkCacheEntry{},
}
newApp.plugins = NewAppPlugins(newApp, plugins, appEntry.Metadata.Accounts)
newApp.appDefaults = appDefaults
if err := newApp.updateAppDefaults(); err != nil {
newApp.appConfig = appConfig
if err := newApp.updateAppConfig(); err != nil {
return nil, err
}

Expand Down Expand Up @@ -213,16 +213,16 @@ func (a *App) Reload(force, immediate bool, dryRun DryRun) (bool, error) {

// Config lock is not present, use default config
a.Debug().Msg("No config lock file found, using default config")
a.Config = apptype.NewAppConfig()
a.codeConfig = apptype.NewCodeConfig()
if a.IsDev {
a.appDev.Config = a.Config
a.appDev.Config = a.codeConfig
a.appDev.SaveConfigLockFile()
}
} else {
// Config lock file is present, read defaults from that
a.Debug().Msg("Config lock file found, using config from lock file")
a.Config = apptype.NewCompatibleAppConfig()
if err := json.Unmarshal(configData, a.Config); err != nil {
a.codeConfig = apptype.NewCompatibleCodeConfig()
if err := json.Unmarshal(configData, a.codeConfig); err != nil {
return false, err
}
}
Expand All @@ -234,7 +234,7 @@ func (a *App) Reload(force, immediate bool, dryRun DryRun) (bool, error) {

if a.IsDev {
// Copy settings into appdev
a.appDev.Config = a.Config
a.appDev.Config = a.codeConfig
a.appDev.CustomLayout = a.CustomLayout

// Initialize style configuration
Expand Down Expand Up @@ -273,14 +273,14 @@ func (a *App) Reload(force, immediate bool, dryRun DryRun) (bool, error) {

// Parse HTML templates if there are HTML routes
if a.usesHtmlTemplate {
baseFiles, err := a.sourceFS.Glob(path.Join(a.Config.Routing.BaseTemplates, "*.go.html"))
baseFiles, err := a.sourceFS.Glob(path.Join(a.codeConfig.Routing.BaseTemplates, "*.go.html"))
if err != nil {
return false, err
}

if len(baseFiles) == 0 {
// No base templates found, use the default unstructured templates
if a.template, err = a.sourceFS.ParseFS(a.funcMap, a.Config.Routing.TemplateLocations...); err != nil {
if a.template, err = a.sourceFS.ParseFS(a.funcMap, a.codeConfig.Routing.TemplateLocations...); err != nil {
return false, err
}
} else {
Expand All @@ -291,7 +291,7 @@ func (a *App) Reload(force, immediate bool, dryRun DryRun) (bool, error) {
}

a.templateMap = make(map[string]*template.Template)
for _, paths := range a.Config.Routing.TemplateLocations {
for _, paths := range a.codeConfig.Routing.TemplateLocations {
files, err := a.sourceFS.Glob(paths)
if err != nil {
return false, err
Expand Down Expand Up @@ -702,18 +702,18 @@ func (a *App) loadStarlark(thread *starlark.Thread, module string, cache map[str
return cacheEntry.globals, cacheEntry.err
}

// updateAppDefaults updates the app defaults from the metadata
// updateAppConfig updates the app defaults from the metadata
// It creates a TOML intermediate string so that the TOML parsing can be used
func (a *App) updateAppDefaults() error {
if len(a.Metadata.AppDefaults) == 0 {
func (a *App) updateAppConfig() error {
if len(a.Metadata.AppConfig) == 0 {
return nil
}

buf := strings.Builder{}
for key, value := range a.Metadata.AppDefaults {
for key, value := range a.Metadata.AppConfig {
buf.WriteString(fmt.Sprintf("%s=\"%s\"\n", key, value))
}

_, err := toml.Decode(buf.String(), &a.appDefaults)
_, err := toml.Decode(buf.String(), &a.appConfig)
return err
}
18 changes: 9 additions & 9 deletions internal/app/apptype/appconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

package apptype

type AppConfig struct {
type CodeConfig struct {
Routing RouteConfig `json:"routing"`
Htmx HtmxConfig `json:"htmx"`
}
Expand All @@ -22,12 +22,12 @@ type HtmxConfig struct {
Version string `json:"version"`
}

// NewAppConfig creates an AppConfig with default values. This config is used when lock
// NewCodeConfig creates an CodeConfig with default values. This config is used when lock
// file is not present. The config file load order is
//
// DefaultAppConfig -> StarlarkAppConfig
func NewAppConfig() *AppConfig {
return &AppConfig{
// DefaultCodeConfig -> StarlarkCodeConfig
func NewCodeConfig() *CodeConfig {
return &CodeConfig{
Routing: RouteConfig{
TemplateLocations: []string{"*.go.html"},
BaseTemplates: "base_templates",
Expand All @@ -41,19 +41,19 @@ func NewAppConfig() *AppConfig {
}
}

// NewCompatibleAppConfig creates an AppConfig focused on maintaining backward compatibility.
// NewCompatibleCodeConfig creates an CodeConfig focused on maintaining backward compatibility.
// This is used when the app is created from a source url where the source has the config lock file
// present. The configs are read in the order
//
// CompatibleAppConfig -> LockFile -> StarlarkAppConfig
// CompatibleCodeConfig -> LockFile -> StarlarkCodeConfig
//
// The goal is that if the application has a lock file, then all settings will attempt to be locked
// such that there should not be any change in behavior when the Clace version is updated.
// Removing the lock file will result in new config defaults getting applied, which can be
// done when the app developer wants to do an application refresh. Refresh will require additional
// testing to ensure that UI functionality is not changed..
func NewCompatibleAppConfig() *AppConfig {
config := NewAppConfig()
func NewCompatibleCodeConfig() *CodeConfig {
config := NewCodeConfig()
config.Htmx.Version = "1.9.1"
return config
}
12 changes: 1 addition & 11 deletions internal/app/container/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,11 @@ func extractVolumes(node *parser.Node) []string {
ret := []string{}
for node.Next != nil {
node = node.Next
ret = append(ret, stripQuotes(node.Value))
ret = append(ret, types.StripQuotes(node.Value))
}
return ret
}

func stripQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' {
return s[1 : len(s)-1]
}
return s
}

func (m *Manager) GetProxyUrl() string {
return fmt.Sprintf("%s://127.0.0.1:%d", m.scheme, m.hostPort)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/dev/appdev.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type AppDev struct {
*types.Logger

CustomLayout bool
Config *apptype.AppConfig
Config *apptype.CodeConfig
systemConfig *types.SystemConfig
sourceFS *appfs.WritableSourceFs
workFS *appfs.WorkFs
Expand Down
6 changes: 3 additions & 3 deletions internal/app/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (a *App) createHandlerFunc(fullHtml, fragment string, handler starlark.Call

isHtmxRequest := r.Header.Get("HX-Request") == "true" && !(r.Header.Get("HX-Boosted") == "true")

if a.Config.Routing.EarlyHints && !a.IsDev && r.Method == http.MethodGet &&
if a.codeConfig.Routing.EarlyHints && !a.IsDev && r.Method == http.MethodGet &&
r.Header.Get("sec-fetch-mode") == "navigate" &&
rtype == apptype.HTML_TYPE && !(isHtmxRequest && fragment != "") {
// Prod mode, for a GET request from newer browsers on a top level HTML page, send http early hints
Expand All @@ -85,8 +85,8 @@ func (a *App) createHandlerFunc(fullHtml, fragment string, handler starlark.Call
Method: r.Method,
IsDev: a.IsDev,
IsPartial: isHtmxRequest,
PushEvents: a.Config.Routing.PushEvents,
HtmxVersion: a.Config.Htmx.Version,
PushEvents: a.codeConfig.Routing.PushEvents,
HtmxVersion: a.codeConfig.Htmx.Version,
Headers: r.Header,
RemoteIP: getRemoteIP(r),
}
Expand Down
Loading

0 comments on commit f774465

Please sign in to comment.