You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lazydocker/pkg/config/app_config.go

598 lines
22 KiB
Go

// Package config handles all the user-configuration. The fields here are
// all in PascalCase but in your actual config.yml they'll be in camelCase.
// You can view the default config with `lazydocker --config`.
// You can open your config file by going to the status panel (using left-arrow)
// and pressing 'o'.
// You can directly edit the file (e.g. in vim) by pressing 'e' instead.
// To see the final config after your user-specific options have been merged
// with the defaults, go to the 'about' tab in the status panel.
// Because of the way we merge your user config with the defaults you may need
// to be careful: if for example you set a `commandTemplates:` yaml key but then
// give it no child values, it will scrap all of the defaults and the app will
// probably crash.
package config
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/OpenPeeDeeP/xdg"
"github.com/jesseduffield/yaml"
)
// UserConfig holds all of the user-configurable options
type UserConfig struct {
// Gui is for configuring visual things like colors and whether we show or
// hide things
Gui GuiConfig `yaml:"gui,omitempty"`
// ConfirmOnQuit when enabled prompts you to confirm you want to quit when you
// hit esc or q when no confirmation panels are open
ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"`
// Logs determines how we render/filter a container's logs
Logs LogsConfig `yaml:"logs,omitempty"`
// CommandTemplates determines what commands actually get called when we run
// certain commands
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
// CustomCommands determines what shows up in your custom commands menu when
// you press 'c'. You can use go templates to access three items on the
// struct: the DockerCompose command (defaulted to 'docker-compose'), the
// Service if present, and the Container if present. The struct types for
// those are found in the commands package
CustomCommands CustomCommands `yaml:"customCommands,omitempty"`
// BulkCommands are commands that apply to all items in a panel e.g.
// killing all containers, stopping all services, or pruning all images
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
// OS determines what defaults are set for opening files and links
OS OSConfig `yaml:"oS,omitempty"`
// Stats determines how long lazydocker will gather container stats for, and
// what stat info to graph
Stats StatsConfig `yaml:"stats,omitempty"`
// Replacements determines how we render an item's info
Replacements Replacements `yaml:"replacements,omitempty"`
// For demo purposes: any list item with one of these strings as a substring
// will be filtered out and not displayed.
// Not documented because it's subject to change
Ignore []string `yaml:"ignore,omitempty"`
}
// ThemeConfig is for setting the colors of panels and some text.
type ThemeConfig struct {
ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"`
InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"`
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
}
// GuiConfig is for configuring visual things like colors and whether we show or
// hide things
type GuiConfig struct {
// ScrollHeight determines how many characters you scroll at a time when
// scrolling the main panel
ScrollHeight int `yaml:"scrollHeight,omitempty"`
// Language determines which language the GUI displayed.
Language string `yaml:"language,omitempty"`
// ScrollPastBottom determines whether you can scroll past the bottom of the
// main view
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
// IgnoreMouseEvents is for when you do not want to use your mouse to interact
// with anything
IgnoreMouseEvents bool `yaml:"mouseEvents,omitempty"`
// Theme determines what colors and color attributes your panel borders have.
// I always set inactiveBorderColor to black because in my terminal it's more
// of a grey, but that doesn't work in your average terminal. I highly
// recommended finding a combination that works for you
Theme ThemeConfig `yaml:"theme,omitempty"`
// ShowAllContainers determines whether the Containers panel contains all the
// containers returned by `docker ps -a`, or just those containers that aren't
// directly linked to a service. It is probably desirable to enable this if
// you have multiple containers per service, but otherwise it can cause a lot
// of clutter
ShowAllContainers bool `yaml:"showAllContainers,omitempty"`
// ReturnImmediately determines whether you get the 'press enter to return to
// lazydocker' message after a subprocess has completed. You would set this to
// true if you often want to see the output of subprocesses before returning
// to lazydocker. I would default this to false but then people who want it
// set to true won't even know the config option exists.
ReturnImmediately bool `yaml:"returnImmediately,omitempty"`
// WrapMainPanel determines whether we use word wrap on the main panel
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
// LegacySortContainers determines if containers should be sorted using legacy approach.
// By default, containers are now sorted by status. This setting allows users to
// use legacy behaviour instead.
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
// If 0.333, then the side panels will be 1/3 of the screen's width
SidePanelWidth float64 `yaml:"sidePanelWidth"`
// Determines whether we show the bottom line (the one containing keybinding
// info and the status of the app).
ShowBottomLine bool `yaml:"showBottomLine"`
// When true, increases vertical space used by focused side panel,
// creating an accordion effect
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
}
// CommandTemplatesConfig determines what commands actually get called when we
// run certain commands
type CommandTemplatesConfig struct {
// RestartService is for restarting a service. docker-compose restart {{
// .Service.Name }} works but I prefer docker-compose up --force-recreate {{
// .Service.Name }}
RestartService string `yaml:"restartService,omitempty"`
// StartService is just like the above but for starting
StartService string `yaml:"startService,omitempty"`
// UpService ups the service (creates and starts)
UpService string `yaml:"upService,omitempty"`
// Runs "docker-compose up -d"
Up string `yaml:"up,omitempty"`
// downs everything
Down string `yaml:"down,omitempty"`
// downs and removes volumes
DownWithVolumes string `yaml:"downWithVolumes,omitempty"`
// DockerCompose is for your docker-compose command. You may want to combine a
// few different docker-compose.yml files together, in which case you can set
// this to "docker-compose -f foo/docker-compose.yml -f
// bah/docker-compose.yml". The reason that the other docker-compose command
// templates all start with {{ .DockerCompose }} is so that they can make use
// of whatever you've set in this value rather than you having to copy and
// paste it to all the other commands
DockerCompose string `yaml:"dockerCompose,omitempty"`
// StopService is the command for stopping a service
StopService string `yaml:"stopService,omitempty"`
// ServiceLogs get the logs for a service. This is actually not currently
// used; we just get the logs of the corresponding container. But we should
// probably support explicitly returning the logs of the service when you've
// selected the service, given that a service may have multiple containers.
ServiceLogs string `yaml:"serviceLogs,omitempty"`
// ViewServiceLogs is for when you want to view the logs of a service as a
// subprocess. This defaults to having no filter, unlike the in-app logs
// commands which will usually filter down to the last hour for the sake of
// performance.
ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"`
// RebuildService is the command for rebuilding a service. Defaults to
// something along the lines of `{{ .DockerCompose }} up --build {{
// .Service.Name }}`
RebuildService string `yaml:"rebuildService,omitempty"`
// RecreateService is for force-recreating a service. I prefer this to
// restarting a service because it will also restart any dependent services
// and ensure they're running before trying to run the service at hand
RecreateService string `yaml:"recreateService,omitempty"`
// AllLogs is for showing what you get from doing `docker-compose logs`. It
// combines all the logs together
AllLogs string `yaml:"allLogs,omitempty"`
// ViewAllLogs is the command we use when you want to see all logs in a subprocess with no filtering
ViewAllLogs string `yaml:"viewAlLogs,omitempty"`
// DockerComposeConfig is the command for viewing the config of your docker
// compose. It basically prints out the yaml from your docker-compose.yml
// file(s)
DockerComposeConfig string `yaml:"dockerComposeConfig,omitempty"`
// CheckDockerComposeConfig is what we use to check whether we are in a
// docker-compose context. If the command returns an error then we clearly
// aren't in a docker-compose config and we then just hide the services panel
// and only show containers
CheckDockerComposeConfig string `yaml:"checkDockerComposeConfig,omitempty"`
// ServiceTop is the command for viewing the processes under a given service
ServiceTop string `yaml:"serviceTop,omitempty"`
}
// OSConfig contains config on the level of the os
type OSConfig struct {
// OpenCommand is the command for opening a file
OpenCommand string `yaml:"openCommand,omitempty"`
// OpenCommand is the command for opening a link
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
}
// GraphConfig specifies how to make a graph of recorded container stats
type GraphConfig struct {
// Min sets the minimum value that you want to display. If you want to set
// this, you should also set MinType to "static". The reason for this is that
// if Min == 0, it's not clear if it has not been set (given that the
// zero-value of an int is 0) or if it's intentionally been set to 0.
Min float64 `yaml:"min,omitempty"`
// Max sets the maximum value that you want to display. If you want to set
// this, you should also set MaxType to "static". The reason for this is that
// if Max == 0, it's not clear if it has not been set (given that the
// zero-value of an int is 0) or if it's intentionally been set to 0.
Max float64 `yaml:"max,omitempty"`
// Height sets the height of the graph in ascii characters
Height int `yaml:"height,omitempty"`
// Caption sets the caption of the graph. If you want to show CPU Percentage
// you could set this to "CPU (%)"
Caption string `yaml:"caption,omitempty"`
// This is the path to the stat that you want to display. It is based on the
// RecordedStats struct in container_stats.go, so feel free to look there to
// see all the options available. Alternatively if you go into lazydocker and
// go to the stats tab, you'll see that same struct in JSON format, so you can
// just PascalCase the path and you'll have a valid path. E.g.
// ClientStats.blkio_stats -> "ClientStats.BlkioStats"
StatPath string `yaml:"statPath,omitempty"`
// This determines the color of the graph. This can be any color attribute,
// e.g. 'blue', 'green'
Color string `yaml:"color,omitempty"`
// MinType and MaxType are each one of "", "static". blank means the min/max
// of the data set will be used. "static" means the min/max specified will be
// used
MinType string `yaml:"minType,omitempty"`
// MaxType is just like MinType but for the max value
MaxType string `yaml:"maxType,omitempty"`
}
// StatsConfig contains the stuff relating to stats and graphs
type StatsConfig struct {
// Graphs contains the configuration for the stats graphs we want to show in
// the app
Graphs []GraphConfig
// MaxDuration tells us how long to collect stats for. Currently this defaults
// to "5m" i.e. 5 minutes.
MaxDuration time.Duration `yaml:"maxDuration,omitempty"`
}
// CustomCommands contains the custom commands that you might want to use on any
// given service or container
type CustomCommands struct {
// Containers contains the custom commands for containers
Containers []CustomCommand `yaml:"containers,omitempty"`
// Services contains the custom commands for services
Services []CustomCommand `yaml:"services,omitempty"`
// Images contains the custom commands for images
Images []CustomCommand `yaml:"images,omitempty"`
// Volumes contains the custom commands for volumes
Volumes []CustomCommand `yaml:"volumes,omitempty"`
// Networks contains the custom commands for networks
Networks []CustomCommand `yaml:"networks,omitempty"`
}
// Replacements contains the stuff relating to rendering a container's info
type Replacements struct {
// ImageNamePrefixes tells us how to replace a prefix in the Docker image name
ImageNamePrefixes map[string]string `yaml:"imageNamePrefixes,omitempty"`
}
// CustomCommand is a template for a command we want to run against a service or
// container
type CustomCommand struct {
// Name is the name of the command, purely for visual display
Name string `yaml:"name"`
// Attach tells us whether to switch to a subprocess to interact with the
// called program, or just read its output. If Attach is set to false, the
// command will run in the background. I'm open to the idea of having a third
// option where the output plays in the main panel.
Attach bool `yaml:"attach"`
// Shell indicates whether to invoke the Command on a shell or not.
// Example of a bash invoked command: `/bin/bash -c "{Command}".
Shell bool `yaml:"shell"`
// Command is the command we want to run. We can use the go templates here as
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
// /bin/sh`
Command string `yaml:"command"`
// ServiceNames is used to restrict this command to just one or more services.
// An example might be 'rails migrate' for your rails api service(s). This
// field has no effect on customcommands under the 'communications' part of
// the customCommand config.
ServiceNames []string `yaml:"serviceNames"`
// InternalFunction is the name of a function inside lazydocker that we want to run, as opposed to a command-line command. This is only used internally and can't be configured by the user
InternalFunction func() error `yaml:"-"`
}
type LogsConfig struct {
Timestamps bool `yaml:"timestamps,omitempty"`
Since string `yaml:"since,omitempty"`
Tail string `yaml:"tail,omitempty"`
}
// GetDefaultConfig returns the application default configuration NOTE (to
// contributors, not users): do not default a boolean to true, because false is
// the boolean zero value and this will be ignored when parsing the user's
// config
func GetDefaultConfig() UserConfig {
duration, err := time.ParseDuration("3m")
if err != nil {
panic(err)
}
return UserConfig{
Gui: GuiConfig{
ScrollHeight: 2,
Language: "auto",
ScrollPastBottom: false,
IgnoreMouseEvents: false,
Theme: ThemeConfig{
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"default"},
OptionsTextColor: []string{"blue"},
},
ShowAllContainers: false,
ReturnImmediately: false,
WrapMainPanel: true,
LegacySortContainers: false,
SidePanelWidth: 0.3333,
ShowBottomLine: true,
ExpandFocusedSidePanel: false,
},
ConfirmOnQuit: false,
Logs: LogsConfig{
Timestamps: false,
Since: "60m",
Tail: "",
},
CommandTemplates: CommandTemplatesConfig{
DockerCompose: "docker-compose",
RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}",
StartService: "{{ .DockerCompose }} start {{ .Service.Name }}",
Up: "{{ .DockerCompose }} up -d",
Down: "{{ .DockerCompose }} down",
DownWithVolumes: "{{ .DockerCompose }} down --volumes",
UpService: "{{ .DockerCompose }} up -d {{ .Service.Name }}",
RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}",
ServiceLogs: "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}",
ViewServiceLogs: "{{ .DockerCompose }} logs --follow {{ .Service.Name }}",
AllLogs: "{{ .DockerCompose }} logs --tail=300 --follow",
ViewAllLogs: "{{ .DockerCompose }} logs",
DockerComposeConfig: "{{ .DockerCompose }} config",
CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet",
ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}",
},
CustomCommands: CustomCommands{
Containers: []CustomCommand{},
Services: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
BulkCommands: CustomCommands{
Services: []CustomCommand{
{
Name: "up",
Command: "{{ .DockerCompose }} up -d",
},
{
Name: "up (attached)",
Command: "{{ .DockerCompose }} up",
Attach: true,
},
{
Name: "stop",
Command: "{{ .DockerCompose }} stop",
},
{
Name: "pull",
Command: "{{ .DockerCompose }} pull",
Attach: true,
},
{
Name: "build",
Command: "{{ .DockerCompose }} build --parallel --force-rm",
Attach: true,
},
{
Name: "down",
Command: "{{ .DockerCompose }} down",
},
{
Name: "down with volumes",
Command: "{{ .DockerCompose }} down --volumes",
},
{
Name: "down with images",
Command: "{{ .DockerCompose }} down --rmi all",
},
{
Name: "down with volumes and images",
Command: "{{ .DockerCompose }} down --volumes --rmi all",
},
},
Containers: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
OS: GetPlatformDefaultConfig(),
Stats: StatsConfig{
MaxDuration: duration,
Graphs: []GraphConfig{
{
Caption: "CPU (%)",
StatPath: "DerivedStats.CPUPercentage",
Color: "cyan",
},
{
Caption: "Memory (%)",
StatPath: "DerivedStats.MemoryPercentage",
Color: "green",
},
},
},
Replacements: Replacements{
ImageNamePrefixes: map[string]string{},
},
}
}
// AppConfig contains the base configuration fields required for lazydocker.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazydocker"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *UserConfig
ConfigDir string
ProjectDir string
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir(name)
if err != nil {
return nil, err
}
userConfig, err := loadUserConfigWithDefaults(configDir)
if err != nil {
return nil, err
}
// Pass compose files as individual -f flags to docker-compose
if len(composeFiles) > 0 {
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
}
appConfig := &AppConfig{
Name: name,
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag || os.Getenv("DEBUG") == "TRUE",
BuildSource: buildSource,
UserConfig: userConfig,
ConfigDir: configDir,
ProjectDir: projectDir,
}
return appConfig, nil
}
func configDirForVendor(vendor string, projectName string) string {
envConfigDir := os.Getenv("CONFIG_DIR")
if envConfigDir != "" {
return envConfigDir
}
configDirs := xdg.New(vendor, projectName)
return configDirs.ConfigHome()
}
func configDir(projectName string) string {
legacyConfigDirectory := configDirForVendor("jesseduffield", projectName)
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
return legacyConfigDirectory
}
configDirectory := configDirForVendor("", projectName)
return configDirectory
}
func findOrCreateConfigDir(projectName string) (string, error) {
folder := configDir(projectName)
err := os.MkdirAll(folder, 0o755)
if err != nil {
return "", err
}
return folder, nil
}
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
config := GetDefaultConfig()
return loadUserConfig(configDir, &config)
}
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
fileName := filepath.Join(configDir, "config.yml")
if _, err := os.Stat(fileName); err != nil {
if os.IsNotExist(err) {
file, err := os.Create(fileName)
if err != nil {
return nil, err
}
file.Close()
} else {
return nil, err
}
}
content, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(content, base); err != nil {
return nil, err
}
return base, nil
}
// WriteToUserConfig allows you to set a value on the user config to be saved
// note that if you set a zero-value, it may be ignored e.g. a false or 0 or
// empty string this is because we are using the omitempty yaml directive so
// that we don't write a heap of zero values to the user's config.yml
func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) error {
userConfig, err := loadUserConfig(c.ConfigDir, &UserConfig{})
if err != nil {
return err
}
if err := updateConfig(userConfig); err != nil {
return err
}
file, err := os.OpenFile(c.ConfigFilename(), os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return err
}
return yaml.NewEncoder(file).Encode(userConfig)
}
// ConfigFilename returns the filename of the current config file
func (c *AppConfig) ConfigFilename() string {
return filepath.Join(c.ConfigDir, "config.yml")
}