diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ecee70..a831b628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ ## Unreleased +## 0.11.0 - 2020-04-29 + +## Added +- Added the `rename` command to allow users to quickly rename sites. + +## Changed +- The `destroy` command now has a `--clean` option which will delete a config file after destroying the machine. +- The `nitro` database user now has root privileges for `mysql` and `postgres` databases. [#79](https://github.com/craftcms/nitro/issues/79) +- Added the `php` option back to the config file. +- All commands that perform config changes (e.g. `add`, `remove`, and `rename`) now use the same logic as the `apply` command. +- When importing a database using the `import` command, users will be prompted for the database name which will be created if it does not exist. +- The `apply` command will automatically update the machine's hosts file. +- The `destroy` command will now remove any sites in the machine config from the hosts file. +- The `init` command will use an existing config file and recreate the entire environment. +- Commands now output more _statuses_ where possible to provide the user more feedback. + +## Fixed +- When using the `add` command, the config file checks for duplicate sites and mounts. [#86](https://github.com/craftcms/nitro/issues/86) +- Fixed an issue when using some commands on Windows. [#88](https://github.com/craftcms/nitro/issues/88) +- Fixed an issue in the `apply` command that would not detect new changes to the config file. + ## 0.10.0 - 2020-04-23 > **Warning:** This release contains breaking changes. See the [upgrade notes](UPGRADE.md#upgrading-to-nitro-0100) diff --git a/config/config.go b/config/config.go index b232ac3b..2d99f5ea 100644 --- a/config/config.go +++ b/config/config.go @@ -14,7 +14,7 @@ import ( ) type Config struct { - PHP string `yaml:"-"` + PHP string `yaml:"php"` CPUs string `yaml:"-"` Disk string `yaml:"-"` Memory string `yaml:"-"` @@ -23,39 +23,60 @@ type Config struct { Sites []Site `yaml:"sites,omitempty"` } -type Mount struct { - Source string `yaml:"source"` - Dest string `yaml:"dest"` +func (c *Config) AddSite(site Site) error { + if len(site.Aliases) == 0 { + site.Aliases = nil + } + + c.Sites = append(c.Sites, site) + return nil } -type Database struct { - Engine string `yaml:"engine"` - Version string `yaml:"version"` - Port string `yaml:"port"` +func (c *Config) GetSites() []Site { + return c.Sites } -type Site struct { - Hostname string `yaml:"hostname"` - Webroot string `yaml:"webroot"` - Aliases []string `yaml:"aliases,omitempty"` +// GetExpandedMounts will take all of the mounts in a config file +// and "expand" or get the full path mount source and return +// a slice of mounts +func (c *Config) GetExpandedMounts() []Mount { + var mounts []Mount + for _, m := range c.Mounts { + mounts = append(mounts, Mount{Source: m.AbsSourcePath(), Dest: m.Dest}) + } + return mounts } -func (m *Mount) AbsSourcePath() string { - home, _ := homedir.Dir() - return strings.Replace(m.Source, "~", home, 1) +// MountExists will check if a mount exists by checking if it is an exact +// dest or a parent of an existing dest +func (c *Config) MountExists(dest string) bool { + for _, mount := range c.Mounts { + if mount.IsExact(dest) || mount.IsParent(dest) { + return true + } + } + + return false } -func (c *Config) AddSite(site Site) error { - if len(site.Aliases) == 0 { - site.Aliases = nil +func (c *Config) SiteExists(site Site) bool { + for _, s := range c.Sites { + if s.IsExact(site) { + return true + } } - c.Sites = append(c.Sites, site) - return nil + return false } -func (c *Config) GetSites() []Site { - return c.Sites +func (c *Config) DatabaseExists(database Database) bool { + for _, d := range c.Databases { + if d.Engine == database.Engine && d.Version == database.Version && d.Port == database.Port { + return true + } + } + + return false } func (c *Config) SitesAsList() []string { @@ -106,6 +127,36 @@ func (c *Config) AddMount(m Mount) error { return nil } +func (c *Config) RenameSite(site Site, hostname string) error { + for i, s := range c.Sites { + if s.Hostname == site.Hostname { + w := strings.Replace(s.Webroot, s.Hostname, hostname, 1) + c.Sites[i] = Site{Hostname: hostname, Webroot: w} + + return nil + } + } + + return errors.New("unable to locate the site with the hostname: " + site.Hostname) +} + +func (c *Config) RenameMountBySite(site Site) error { + for i, mount := range c.Mounts { + sp := strings.Split(site.Webroot, "/") + siteMount := sp[len(sp)-1] + if strings.Contains(mount.Dest, siteMount) { + c.Mounts[i] = Mount{ + Source: mount.Source, + Dest: siteMount, + } + + return nil + } + } + + return errors.New("unable to find the mount for the site " + site.Hostname) +} + func (c *Config) RemoveSite(hostname string) error { for i := len(c.Sites) - 1; i >= 0; i-- { site := c.Sites[i] diff --git a/config/config_test.go b/config/config_test.go index da29b8d9..c2a13bed 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -538,3 +538,299 @@ func TestConfig_RemoveSite1(t *testing.T) { }) } } + +func TestConfig_RenameSite(t *testing.T) { + type fields struct { + Mounts []Mount + Databases []Database + Sites []Site + } + type args struct { + site Site + hostname string + } + tests := []struct { + name string + fields fields + args args + want []Site + wantErr bool + }{ + { + name: "remove a site my hostname", + args: args{ + site: Site{ + Hostname: "old.test", + Webroot: "/nitro/sites/old.test", + }, + hostname: "new.test", + }, + fields: fields{ + Sites: []Site{ + { + Hostname: "old.test", + Webroot: "/nitro/sites/old.test", + }, + { + Hostname: "keep.test", + Webroot: "/nitro/sites/keep.test", + }, + }, + }, + want: []Site{ + { + Hostname: "new.test", + Webroot: "/nitro/sites/new.test", + }, + { + Hostname: "keep.test", + Webroot: "/nitro/sites/keep.test", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + Mounts: tt.fields.Mounts, + Databases: tt.fields.Databases, + Sites: tt.fields.Sites, + } + if err := c.RenameSite(tt.args.site, tt.args.hostname); (err != nil) != tt.wantErr { + t.Errorf("RenameSite() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.want != nil { + if !reflect.DeepEqual(c.Sites, tt.want) { + t.Errorf("RenameSite() got sites = \n%v, \nwant \n%v", c.Sites, tt.want) + } + } + }) + } +} + +func TestConfig_MountExists(t *testing.T) { + type fields struct { + PHP string + CPUs string + Disk string + Memory string + Mounts []Mount + Databases []Database + Sites []Site + } + type args struct { + dest string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "existing mounts return true", + fields: fields{ + Mounts: []Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/example-site", + }, + }, + }, + args: args{dest: "/nitro/sites/example-site"}, + want: true, + }, + { + name: "non-existing mounts return false", + fields: fields{ + Mounts: []Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/example-site", + }, + }, + }, + args: args{dest: "/nitro/sites/nonexistent-site"}, + want: false, + }, + { + name: "parent mounts return true", + fields: fields{ + Mounts: []Mount{ + { + Source: "./testdata/test-mount", + Dest: "/nitro/sites", + }, + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites", + }, + }, + }, + args: args{dest: "/nitro/sites/nonexistent-site"}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + PHP: tt.fields.PHP, + CPUs: tt.fields.CPUs, + Disk: tt.fields.Disk, + Memory: tt.fields.Memory, + Mounts: tt.fields.Mounts, + Databases: tt.fields.Databases, + Sites: tt.fields.Sites, + } + if got := c.MountExists(tt.args.dest); got != tt.want { + t.Errorf("MountExists() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConfig_SiteExists(t *testing.T) { + type fields struct { + PHP string + CPUs string + Disk string + Memory string + Mounts []Mount + Databases []Database + Sites []Site + } + type args struct { + site Site + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "exact sites return true", + fields: fields{ + Sites: []Site{ + { + Hostname: "iexist.test", + Webroot: "/nitro/sites/iexist.test", + }, + }, + }, + args: args{site: Site{ + Hostname: "iexist.test", + Webroot: "/nitro/sites/iexist.test", + }}, + want: true, + }, + { + name: "exact sites return false", + fields: fields{ + Sites: []Site{ + { + Hostname: "iexist.test", + Webroot: "/nitro/sites/iexist.test", + }, + }, + }, + args: args{site: Site{ + Hostname: "idontexist.test", + Webroot: "/nitro/sites/idontexist.test", + }}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + PHP: tt.fields.PHP, + CPUs: tt.fields.CPUs, + Disk: tt.fields.Disk, + Memory: tt.fields.Memory, + Mounts: tt.fields.Mounts, + Databases: tt.fields.Databases, + Sites: tt.fields.Sites, + } + if got := c.SiteExists(tt.args.site); got != tt.want { + t.Errorf("SiteExists() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConfig_DatabaseExists(t *testing.T) { + type fields struct { + PHP string + CPUs string + Disk string + Memory string + Mounts []Mount + Databases []Database + Sites []Site + } + type args struct { + database Database + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "can find an existing database", + fields: fields{ + Databases: []Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + }, + }, + args: args{database: Database{ + Engine: "mysql", + Version: "5.8", + Port: "3306", + }}, + want: false, + }, + { + name: "non-existing databases return false", + fields: fields{ + Databases: []Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + }, + }, + args: args{database: Database{ + Engine: "mysql", + Version: "5.7", + Port: "3306", + }}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Config{ + PHP: tt.fields.PHP, + CPUs: tt.fields.CPUs, + Disk: tt.fields.Disk, + Memory: tt.fields.Memory, + Mounts: tt.fields.Mounts, + Databases: tt.fields.Databases, + Sites: tt.fields.Sites, + } + if got := c.DatabaseExists(tt.args.database); got != tt.want { + t.Errorf("DatabaseExists() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/config/database.go b/config/database.go new file mode 100644 index 00000000..8e301ce8 --- /dev/null +++ b/config/database.go @@ -0,0 +1,14 @@ +package config + +import "fmt" + +type Database struct { + Engine string `yaml:"engine"` + Version string `yaml:"version"` + Port string `yaml:"port"` +} + +// Name converts a database into a name used for the container +func (d *Database) Name() string { + return fmt.Sprintf("%s_%s_%s", d.Engine, d.Version, d.Port) +} diff --git a/config/mount.go b/config/mount.go new file mode 100644 index 00000000..849ee67f --- /dev/null +++ b/config/mount.go @@ -0,0 +1,33 @@ +package config + +import ( + "strings" + + "github.com/mitchellh/go-homedir" +) + +type Mount struct { + Source string `yaml:"source"` + Dest string `yaml:"dest"` +} + +func (m *Mount) AbsSourcePath() string { + home, _ := homedir.Dir() + return strings.Replace(m.Source, "~", home, 1) +} + +func (m *Mount) IsExact(dest string) bool { + if m.Dest == dest { + return true + } + + return false +} + +func (m *Mount) IsParent(dest string) bool { + if strings.Contains(dest, m.Dest) { + return true + } + + return false +} diff --git a/config/site.go b/config/site.go new file mode 100644 index 00000000..55e145f3 --- /dev/null +++ b/config/site.go @@ -0,0 +1,15 @@ +package config + +type Site struct { + Hostname string `yaml:"hostname"` + Webroot string `yaml:"webroot"` + Aliases []string `yaml:"aliases,omitempty"` +} + +func (s *Site) IsExact(site Site) bool { + if s.Hostname == site.Hostname && s.Webroot == site.Webroot { + return true + } + + return false +} diff --git a/config/testdata/configs/full-example.yaml b/config/testdata/configs/full-example.yaml index b68b12aa..fb074429 100644 --- a/config/testdata/configs/full-example.yaml +++ b/config/testdata/configs/full-example.yaml @@ -1,3 +1,4 @@ +php: "7.4" mounts: - source: ~/go/src/github.com/craftcms/nitro/demo-site dest: /nitro/sites/demo-site diff --git a/config/testdata/configs/golden-full.yaml b/config/testdata/configs/golden-full.yaml index 4c08b1ae..cf9741cc 100644 --- a/config/testdata/configs/golden-full.yaml +++ b/config/testdata/configs/golden-full.yaml @@ -1,3 +1,4 @@ +php: "7.4" mounts: - source: ~/go/src/github.com/craftcms/nitro/production-site dest: /nitro/sites/production-site diff --git a/config/testdata/new-mount/.gitkeep b/config/testdata/new-mount/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/go.mod b/go.mod index 8b136560..a9a0742b 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/craftcms/nitro go 1.14 require ( - github.com/manifoldco/promptui v0.7.0 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.6.2 github.com/stretchr/testify v1.5.1 // indirect + github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 github.com/txn2/txeh v1.3.0 gopkg.in/yaml.v2 v2.2.8 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c diff --git a/go.sum b/go.sum index c3acb387..c57acda7 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,6 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -59,8 +53,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 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/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -71,17 +63,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= -github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -134,6 +118,8 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 h1:RB0v+/pc8oMzPsN97aZYEwNuJ6ouRJ2uhjxemJ9zvrY= +github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8/go.mod h1:IlWNj9v/13q7xFbaK4mbyzMNwrZLaWSHx/aibKIZuIg= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/txn2/txeh v1.3.0 h1:vnbv63htVMZCaQgLqVBxKvj2+HHHFUzNW7I183zjg3E= github.com/txn2/txeh v1.3.0/go.mod h1:O7M6gUTPeMF+vsa4c4Ipx3JDkOYrruB1Wry8QRsMcw8= @@ -147,6 +133,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -163,7 +150,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -195,4 +181,5 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 h1:XJP7lxbSxWLOMNdBE4B/STaqVy6L73o0knwj2vIlxnw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/cmd/add.go b/internal/cmd/add.go index 02032ad9..deb97002 100644 --- a/internal/cmd/add.go +++ b/internal/cmd/add.go @@ -2,25 +2,27 @@ package cmd import ( "fmt" - "os/exec" + "os" - "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tcnksm/go-input" "github.com/craftcms/nitro/config" "github.com/craftcms/nitro/internal/helpers" - "github.com/craftcms/nitro/internal/nitro" - "github.com/craftcms/nitro/internal/sudo" + "github.com/craftcms/nitro/internal/prompt" "github.com/craftcms/nitro/internal/webroot" - "github.com/craftcms/nitro/validate" ) var addCommand = &cobra.Command{ Use: "add", Short: "Add site to machine", RunE: func(cmd *cobra.Command, args []string) error { - machine := flagMachineName + // load the config + var configFile config.Config + if err := viper.Unmarshal(&configFile); err != nil { + return err + } // if there is no arg, get the current working dir // else get the first arg @@ -30,54 +32,38 @@ var addCommand = &cobra.Command{ return err } + ui := &input.UI{ + Writer: os.Stdout, + Reader: os.Stdin, + } + // prompt for the hostname if --hostname == "" // else get the name of the current directory (e.g. nitro) var hostname string switch flagHostname { case "": - hostnamePrompt := promptui.Prompt{ - Label: fmt.Sprintf("What should the hostname be? [%s]", directoryName), - Validate: validate.Hostname, - } - - hostnameEntered, err := hostnamePrompt.Run() + hostname, err = prompt.Ask(ui, "What should the hostname be?", directoryName, true) if err != nil { return err } - - switch hostnameEntered { - case "": - hostname = directoryName - default: - hostname = hostnameEntered - } default: hostname = helpers.RemoveTrailingSlash(flagHostname) } - // look for the www,public,public_html,www using the absolutePath variable // set the webrootName var (e.g. web) var webrootDir string switch flagWebroot { case "": + // look for the www,public,public_html,www using the absolutePath variable foundDir, err := webroot.Find(absolutePath) if err != nil { return err } - webRootPrompt := promptui.Prompt{ - Label: fmt.Sprintf("Where is the webroot? [%s]", foundDir), - } - webrootEntered, err := webRootPrompt.Run() + webrootDir, err = prompt.Ask(ui, "Where is the webroot?", foundDir, true) if err != nil { return err } - switch webrootEntered { - case "": - webrootDir = foundDir - default: - webrootDir = webrootEntered - } default: webrootDir = flagWebroot } @@ -85,110 +71,57 @@ var addCommand = &cobra.Command{ // create the vmWebRootPath (e.g. "/nitro/sites/"+ hostName + "/" | webrootName webRootPath := fmt.Sprintf("/nitro/sites/%s/%s", hostname, webrootDir) - // load the config - var configFile config.Config - if err := viper.Unmarshal(&configFile); err != nil { - return err - } - // create a new mount - // add the mount to configfile + skipMount := true mount := config.Mount{Source: absolutePath, Dest: "/nitro/sites/" + hostname} - if err := configFile.AddMount(mount); err != nil { - return err + if configFile.MountExists(mount.Dest) { + fmt.Println(mount.Source, "is already mounted at", mount.Dest, ". Using that instead of creating a new mount.") + } else { + // add the mount to configfile + if err := configFile.AddMount(mount); err != nil { + return err + } + skipMount = false } // create a new site // add site to config file + skipSite := true site := config.Site{Hostname: hostname, Webroot: webRootPath} - if err := configFile.AddSite(site); err != nil { - return err - } - - if !flagDebug { - if err := configFile.Save(viper.ConfigFileUsed()); err != nil { + if configFile.SiteExists(site) { + fmt.Println(site.Hostname, "has already been set") + } else { + if err := configFile.AddSite(site); err != nil { return err } + skipSite = false } - fmt.Printf("%s has been added to nitro.yaml", hostname) - - applyPrompt := promptui.Prompt{ - Label: "Apply changes now? [y]", - } - - apply, err := applyPrompt.Run() - if err != nil { - return err - } - if apply == "" { - apply = "y" - } - - if apply != "y" { - fmt.Println("You can apply new nitro.yaml changes later by running `nitro apply`.") - + if skipMount && skipSite { + fmt.Println("There are no changes to apply, skipping...") return nil } - var actions []nitro.Action - // mount the directory - m := configFile.Mounts[len(configFile.Mounts)-1] - mountAction, err := nitro.MountDir(machine, m.AbsSourcePath(), m.Dest) - if err != nil { - return err - } - actions = append(actions, *mountAction) - - // copy the nginx template - copyTemplateAction, err := nitro.CopyNginxTemplate(machine, site.Hostname) - if err != nil { - return err - } - actions = append(actions, *copyTemplateAction) - - // copy the nginx template - changeNginxVariablesAction, err := nitro.ChangeTemplateVariables(machine, site.Webroot, site.Hostname, configFile.PHP, site.Aliases) - if err != nil { - return err + if !flagDebug { + if err := configFile.Save(viper.ConfigFileUsed()); err != nil { + return err + } } - actions = append(actions, *changeNginxVariablesAction...) - createSymlinkAction, err := nitro.CreateSiteSymllink(machine, site.Hostname) - if err != nil { - return err - } - actions = append(actions, *createSymlinkAction) + fmt.Printf("%s has been added to nitro.yaml\n", hostname) - restartNginxAction, err := nitro.NginxReload(machine) + applyChanges, err := prompt.Verify(ui, "Apply changes from config now?", "y") if err != nil { return err } - actions = append(actions, *restartNginxAction) - if flagDebug { - for _, action := range actions { - fmt.Println(action.Args) - } + if !applyChanges { + fmt.Println("You can apply new nitro.yaml changes later by running `nitro apply`.") return nil } - if err = nitro.Run(nitro.NewMultipassRunner("multipass"), actions); err != nil { - return err - } - - fmt.Println("Applied the changes and added", hostname, "to", machine) - - // prompt to add hosts file - nitro, err := exec.LookPath("nitro") - if err != nil { - return err - } - - fmt.Println("Adding", site.Hostname, "to your hosts file") - - return sudo.RunCommand(nitro, machine, "hosts") + return applyCommand.RunE(cmd, args) }, } diff --git a/internal/cmd/apply.go b/internal/cmd/apply.go index 4f88b5a8..62eeee0e 100644 --- a/internal/cmd/apply.go +++ b/internal/cmd/apply.go @@ -9,132 +9,75 @@ import ( "github.com/spf13/viper" "github.com/craftcms/nitro/config" - "github.com/craftcms/nitro/internal/diff" "github.com/craftcms/nitro/internal/find" "github.com/craftcms/nitro/internal/nitro" + "github.com/craftcms/nitro/internal/sudo" + "github.com/craftcms/nitro/internal/task" ) var applyCommand = &cobra.Command{ - Use: "apply", - Short: "Apply changes from config", + Use: "apply", + Short: "Apply changes from config", RunE: func(cmd *cobra.Command, args []string) error { machine := flagMachineName - path, err := exec.LookPath("multipass") - if err != nil { + // always read the config file so its updated from any previous commands + if err := viper.ReadInConfig(); err != nil { return err } - c := exec.Command(path, []string{"info", machine, "--format=csv"}...) - output, err := c.Output() - if err != nil { + // load the config file + var configFile config.Config + if err := viper.Unmarshal(&configFile); err != nil { return err } - attachedMounts, err := find.Mounts(machine, output) + // ABSTRACT + path, err := exec.LookPath("multipass") if err != nil { return err } - // load the config file - var configFile config.Config - if err := viper.Unmarshal(&configFile); err != nil { + c := exec.Command(path, []string{"info", machine, "--format=csv"}...) + output, err := c.Output() + if err != nil { return err } - // get abs path for file sources - var fileMounts []config.Mount - for _, m := range configFile.Mounts { - fileMounts = append(fileMounts, config.Mount{Source: m.AbsSourcePath(), Dest: m.Dest}) + // find mounts that already exist + mounts, err := find.Mounts(machine, output) + if err != nil { + return err } + // END ABSTRACT - // find sites not created - var sitesToCreate []config.Site + // find sites that are created + var sites []config.Site for _, site := range configFile.Sites { - c := exec.Command(path, "exec", machine, "--", "sudo", "bash", "/opt/nitro/scripts/site-exists.sh", site.Hostname) - output, err := c.Output() + output, err := exec.Command(path, "exec", machine, "--", "sudo", "bash", "/opt/nitro/scripts/site-exists.sh", site.Hostname).Output() if err != nil { return err } - if !strings.Contains(string(output), "exists") { - sitesToCreate = append(sitesToCreate, site) + if strings.Contains(string(output), "exists") { + sites = append(sites, site) } } - // check for new dbs - dbsToCreate, err := find.ContainersToCreate(machine, configFile) + // find all existing databases + databases, err := find.AllDatabases(exec.Command(path, []string{"exec", machine, "--", "docker", "container", "ls", "--format", `'{{ .Names }}'`}...)) if err != nil { return err } - // prompt? - var actions []nitro.Action - - mountActions, err := diff.MountActions(machine, attachedMounts, fileMounts) + // find the current version of php installed + php, err := find.PHPVersion(exec.Command(path, "exec", machine, "--", "php", "--version")) if err != nil { return err } - actions = append(actions, mountActions...) - - // create site actions - for _, site := range sitesToCreate { - // TODO abstract this logic into a func that takes mountActions and sites to return the mount action - for _, ma := range mountActions { - // break the string - mnt := strings.Split(ma.Args[2], ":") - - // if the webroot is not of the mounts, then we should create an action - if !strings.Contains(mnt[1], site.Webroot) { - m := configFile.FindMountBySiteWebroot(site.Webroot) - mountAction, err := nitro.MountDir(machine, m.AbsSourcePath(), m.Dest) - if err != nil { - return err - } - actions = append(actions, *mountAction) - } - } - - copyTemplateAction, err := nitro.CopyNginxTemplate(machine, site.Hostname) - if err != nil { - return err - } - actions = append(actions, *copyTemplateAction) - - // copy the nginx template - changeNginxVariablesAction, err := nitro.ChangeTemplateVariables(machine, site.Webroot, site.Hostname, configFile.PHP, site.Aliases) - if err != nil { - return err - } - actions = append(actions, *changeNginxVariablesAction...) - - createSymlinkAction, err := nitro.CreateSiteSymllink(machine, site.Hostname) - if err != nil { - return err - } - actions = append(actions, *createSymlinkAction) - } - - if len(sitesToCreate) > 0 { - restartNginxAction, err := nitro.NginxReload(machine) - if err != nil { - return err - } - actions = append(actions, *restartNginxAction) - } - - // create database actions - for _, database := range dbsToCreate { - volumeAction, err := nitro.CreateDatabaseVolume(machine, database.Engine, database.Version, database.Port) - if err != nil { - return err - } - actions = append(actions, *volumeAction) - createDatabaseAction, err := nitro.CreateDatabaseContainer(machine, database.Engine, database.Version, database.Port) - if err != nil { - return err - } - actions = append(actions, *createDatabaseAction) + actions, err := task.Apply(machine, configFile, mounts, sites, databases, php) + if err != nil { + return err } if flagDebug { @@ -145,15 +88,20 @@ var applyCommand = &cobra.Command{ return nil } - fmt.Printf("There are %d mounted directories and %d mounts in the config file. Applying changes now...\n", len(attachedMounts), len(fileMounts)) - fmt.Printf("There are %d sites to create and %d sites in the config file. Applying changes now...\n", len(sitesToCreate), len(configFile.Sites)) - if err := nitro.Run(nitro.NewMultipassRunner("multipass"), actions); err != nil { return err } fmt.Println("Applied changes from", viper.ConfigFileUsed()) - return nil + nitro, err := exec.LookPath("nitro") + if err != nil { + return err + } + + fmt.Println("Editing your hosts file") + + // TODO check the current OS and call commands for windows + return sudo.RunCommand(nitro, machine, "hosts") }, } diff --git a/internal/cmd/config.go b/internal/cmd/config.go index f74c201c..884807d8 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -35,9 +35,38 @@ write_files: engine="$4" if [ "$engine" == "mysql" ]; then - cat "$filename" | pv | docker exec -i "$container" mysql -unitro -pnitro "$database" --init-command="SET autocommit=0;" + docker exec "$container" mysql -uroot -pnitro -e "CREATE DATABASE IF NOT EXISTS $database;" + docker exec "$container" mysql -uroot -pnitro -e "GRANT ALL ON $database.* TO 'nitro'@'%';" + docker exec "$container" mysql -uroot -pnitro -e "FLUSH PRIVILEGES;" + cat "$filename" | pv | docker exec "$container" mysql -unitro -pnitro "$database" --init-command="SET autocommit=0;" else - cat "$filename" | pv | docker exec -i "$container" psql -U nitro -d "$database" + docker exec "$container" psql -U nitro -c "CREATE DATABASE IF NOT EXISTS $database OWNER nitro;" + cat "$filename" | pv | docker exec "$container" psql -U nitro -d "$database" + fi + - path: /opt/nitro/scripts/docker-set-database-user-permissions.sh + content: | + #!/usr/bin/env bash + container="$1" + engine="$2" + + if [ -z "$container" ]; then + echo "you must provide a container name" + exit 1 + fi + + if [ -z "$engine" ]; then + echo "you must provide a database engine (e.g. mysql or postgres)" + exit 1 + fi + + if [ "$engine" == "mysql" ]; then + docker exec "$container" bash -c "while ! mysqladmin ping -h 127.0.0.1 -uroot -pnitro; do echo 'waiting...'; sleep 1; done" + docker exec "$container" mysql -uroot -pnitro -e "GRANT ALL ON *.* TO 'nitro'@'%';" + docker exec "$container" mysql -uroot -pnitro -e "FLUSH PRIVILEGES;" + echo "setting root permissions on user nitro" + else + docker exec "$container" psql -U nitro -c "ALTER USER nitro WITH SUPERUSER;" + echo "setting superuser permissions on user nitro" fi - path: /opt/nitro/nginx/template.conf content: | diff --git a/internal/cmd/context.go b/internal/cmd/context.go index ef1d9961..e5454c2d 100644 --- a/internal/cmd/context.go +++ b/internal/cmd/context.go @@ -24,7 +24,7 @@ var contextCommand = &cobra.Command{ return err } - fmt.Println("Using config file:", configFile) + fmt.Println("Using config:", configFile) fmt.Println("------") fmt.Print(string(data)) return nil diff --git a/internal/cmd/destroy.go b/internal/cmd/destroy.go index 5e406045..15b1cd50 100644 --- a/internal/cmd/destroy.go +++ b/internal/cmd/destroy.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "os/exec" "github.com/spf13/cobra" @@ -38,7 +39,14 @@ var destroyCommand = &cobra.Command{ return err } + if flagClean { + if err := os.Remove(viper.ConfigFileUsed()); err != nil { + fmt.Println("unable to remove the config:", viper.ConfigFileUsed()) + } + } + if len(domains) == 0 { + fmt.Println("Permanently removed", machine) return nil } @@ -58,3 +66,7 @@ var destroyCommand = &cobra.Command{ return sudo.RunCommand(nitro, machine, cmds...) }, } + +func init() { + destroyCommand.Flags().BoolVar(&flagClean, "clean", false, "remove the config file when destroying the machine") +} diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go index 6db87c15..3d95be4c 100644 --- a/internal/cmd/flags.go +++ b/internal/cmd/flags.go @@ -8,6 +8,7 @@ var ( flagDisk string flagPhpVersion string flagNginxLogsKind string + flagClean bool // flags for the add command flagHostname string diff --git a/internal/cmd/hosts.go b/internal/cmd/hosts.go index 3a286666..d436db1e 100644 --- a/internal/cmd/hosts.go +++ b/internal/cmd/hosts.go @@ -32,15 +32,16 @@ var hostsCommand = &cobra.Command{ ip := nitro.IP(machine, nitro.NewMultipassRunner("multipass")) // get all of the sites from the config file - if !viper.IsSet("sites") { - return errors.New("unable to read sites from " + viper.ConfigFileUsed()) - } - var sites []config.Site if err := viper.UnmarshalKey("sites", &sites); err != nil { return err } + if sites == nil { + fmt.Println("There are no sites in the config file to remove") + return nil + } + var domains []string for _, site := range sites { domains = append(domains, site.Hostname) diff --git a/internal/cmd/import.go b/internal/cmd/import.go index 09d0fa9b..b91bda82 100644 --- a/internal/cmd/import.go +++ b/internal/cmd/import.go @@ -6,15 +6,16 @@ import ( "os" "strings" - "github.com/manifoldco/promptui" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tcnksm/go-input" "github.com/craftcms/nitro/config" "github.com/craftcms/nitro/internal/helpers" "github.com/craftcms/nitro/internal/nitro" "github.com/craftcms/nitro/internal/normalize" + "github.com/craftcms/nitro/internal/prompt" ) var importCommand = &cobra.Command{ @@ -47,14 +48,20 @@ var importCommand = &cobra.Command{ } var dbs []string for _, db := range databases { - dbs = append(dbs, fmt.Sprintf("%s_%s_%s", db.Engine, db.Version, db.Port)) + dbs = append(dbs, db.Name()) } - databaseContainerName := promptui.Select{ - Label: "Select database", - Items: dbs, + ui := &input.UI{ + Writer: os.Stdout, + Reader: os.Stdin, } - _, containerName, err := databaseContainerName.Run() + if len(dbs) == 0 { + return errors.New("there are no databases that we can import the file into") + } + + containerName, _, err := prompt.Select(ui, "Select a database engine to import the file into", dbs[0], dbs) + + databaseName, err := prompt.Ask(ui, "What is the database name?", "", true) if err != nil { return err } @@ -74,7 +81,7 @@ var importCommand = &cobra.Command{ engine = "postgres" } - importArgs := []string{"exec", machine, "--", "bash", "/opt/nitro/scripts/docker-exec-import.sh", containerName, "nitro", filename, engine} + importArgs := []string{"exec", machine, "--", "bash", "/opt/nitro/scripts/docker-exec-import.sh", containerName, databaseName, filename, engine} dockerExecAction := nitro.Action{ Type: "exec", UseSyscall: false, @@ -84,22 +91,12 @@ var importCommand = &cobra.Command{ fmt.Printf("Importing %q into %q (large files may take a while)...\n", filename, containerName) - return nitro.Run(nitro.NewMultipassRunner("multipass"), actions) - }, -} + if err := nitro.Run(nitro.NewMultipassRunner("multipass"), actions); err != nil { + return err + } -func fileExists(filename string) bool { - info, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} + fmt.Println("Successfully imported the database file into", containerName) -func dirExists(dir string) bool { - info, err := os.Stat(dir) - if os.IsExist(err) { - return false - } - return info.IsDir() + return nil + }, } diff --git a/internal/cmd/init.go b/internal/cmd/init.go index 39aa9e60..9ee0633c 100644 --- a/internal/cmd/init.go +++ b/internal/cmd/init.go @@ -1,14 +1,15 @@ package cmd import ( - "errors" "fmt" + "os" "strconv" "strings" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tcnksm/go-input" "github.com/craftcms/nitro/config" "github.com/craftcms/nitro/internal/nitro" @@ -22,9 +23,15 @@ var initCommand = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { machine := flagMachineName + existingConfig := false if viper.ConfigFileUsed() != "" { - // TODO prompt for the confirmation of re initing the machine - return errors.New("using a config file already") + fmt.Println("Using an existing config:", viper.ConfigFileUsed()) + existingConfig = true + } + + ui := &input.UI{ + Writer: os.Stdout, + Reader: os.Stdin, } // we don't have a config file @@ -41,60 +48,103 @@ var initCommand = &cobra.Command{ cfg.CPUs = hardCodedCpus // ask how much memory - memory, err := prompt.AskWithDefault("How much memory should we assign?", "4G", nil) + memory, err := prompt.Ask(ui, "How much memory should we assign?", "4G", true) if err != nil { return err } cfg.Memory = memory // how much disk space - disk, err := prompt.AskWithDefault("How much disk space should the machine have?", "40G", nil) + disk, err := prompt.Ask(ui, "How much disk space should the machine have?", "40G", true) if err != nil { return err } cfg.Disk = disk // which version of PHP - _, php := prompt.SelectWithDefault("Which version of PHP should we install?", "7.4", nitro.PHPVersions) - cfg.PHP = php + if !existingConfig { + php, _, err := prompt.Select(ui, "Which version of PHP should we install?", "7.4", nitro.PHPVersions) + if err != nil { + return err + } + cfg.PHP = php + } else { + cfg.PHP = config.GetString("php", flagPhpVersion) - // what database engine? - _, engine := prompt.SelectWithDefault("Which database engine should we setup?", "mysql", nitro.DBEngines) + // double check from the major update + if cfg.PHP == "" { + cfg.PHP = "7.4" + } + } - // which version should we use? - versions := nitro.DBVersions[engine] - defaultVersion := versions[0] - _, version := prompt.SelectWithDefault("Select a version of "+engine+" to use:", defaultVersion, versions) + if !existingConfig { + // what database engine? + engine, _, err := prompt.Select(ui, "Which database engine should we setup?", "mysql", nitro.DBEngines) + if err != nil { + return err + } - // get the port for the engine - port := "3306" - if strings.Contains(engine, "postgres") { - port = "5432" - } - // TODO check if the port has already been used and +1 it + // which version should we use? + versions := nitro.DBVersions[engine] + defaultVersion := versions[0] + version, _, err := prompt.Select(ui, "Select a version of "+engine+" to use:", defaultVersion, versions) + if err != nil { + return err + } + + // get the port for the engine + port := "3306" + if strings.Contains(engine, "postgres") { + port = "5432" + } - cfg.Databases = []config.Database{ - { - Engine: engine, - Version: version, - Port: port, - }, + cfg.Databases = []config.Database{ + { + Engine: engine, + Version: version, + Port: port, + }, + } + } else { + var databases []config.Database + if err := viper.UnmarshalKey("databases", &databases); err != nil { + return err + } + + if databases != nil { + cfg.Databases = databases + } } - if err := validate.DatabaseConfig(cfg.Databases); err != nil { - return err + if len(cfg.Databases) > 0 { + if err := validate.DatabaseConfig(cfg.Databases); err != nil { + return err + } } - // save the config file - home, err := homedir.Dir() - if err != nil { - return err + var mounts []config.Mount + var sites []config.Site + if existingConfig { + if err := viper.UnmarshalKey("mounts", &mounts); err != nil { + return err + } + if err := viper.UnmarshalKey("sites", &sites); err != nil { + return err + } } - if err := cfg.SaveAs(home, machine); err != nil { - return err + + // save the config file if it does not exist + if !existingConfig { + home, err := homedir.Dir() + if err != nil { + return err + } + if err := cfg.SaveAs(home, machine); err != nil { + return err + } } - actions, err := createActions(machine, memory, disk, cpuInt, php, cfg.Databases, nil, nil) + actions, err := createActions(machine, memory, disk, cpuInt, cfg.PHP, cfg.Databases, mounts, sites) if err != nil { return err } @@ -181,6 +231,12 @@ func createActions(machine, memory, disk string, cpus int, phpVersion string, da return nil, err } actions = append(actions, *createDatabaseAction) + + setUserPermissions, err := nitro.SetDatabaseUserPermissions(machine, database) + if err != nil { + return nil, err + } + actions = append(actions, *setUserPermissions) } var siteErrs []error diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index 3368ceb1..013fa617 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -3,14 +3,15 @@ package cmd import ( "errors" "fmt" - "strings" + "os" - "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tcnksm/go-input" "github.com/craftcms/nitro/config" "github.com/craftcms/nitro/internal/nitro" + "github.com/craftcms/nitro/internal/prompt" ) var logsCommand = &cobra.Command{ @@ -21,14 +22,12 @@ var logsCommand = &cobra.Command{ // define the flags opts := []string{"nginx", "database", "docker"} - - logType := promptui.Select{ - Label: "Select the type of logs to view", - Items: opts, - Size: len(opts), + ui := &input.UI{ + Writer: os.Stdout, + Reader: os.Stdin, } - _, kind, err := logType.Run() + kind, _, err := prompt.Select(ui, "Select the type of logs to view", "nginx", opts) if err != nil { return err } @@ -36,26 +35,15 @@ var logsCommand = &cobra.Command{ var actions []nitro.Action switch kind { case "docker": - validate := func(input string) error { - if input == "" { - return errors.New("container machine cannot be empty") - } - if strings.Contains(input, " ") { - return errors.New("container names cannot contain spaces") - } - return nil - } - - containerNamePrompt := promptui.Prompt{ - Label: "Enter container machine", - Validate: validate, - } - - containerName, err := containerNamePrompt.Run() + containerName, err := prompt.Ask(ui, "Enter container name:", "", true) if err != nil { return err } + if containerName == "" { + return errors.New("container name cannot be empty") + } + dockerLogsAction, err := nitro.LogsDocker(machine, containerName) if err != nil { return err @@ -69,17 +57,18 @@ var logsCommand = &cobra.Command{ } var dbs []string for _, db := range databases { - dbs = append(dbs, fmt.Sprintf("%s_%s_%s", db.Engine, db.Version, db.Port)) + dbs = append(dbs, db.Name()) } - databaseContainerName := promptui.Select{ - Label: "Select database", - Items: dbs, + + if len(dbs) == 0 { + return errors.New("there are no databases to view logs from") } - _, containerName, err := databaseContainerName.Run() + containerName, _, err := prompt.Select(ui, "Select database", dbs[0], dbs) if err != nil { return err } + dockerLogsAction, err := nitro.LogsDocker(machine, containerName) if err != nil { return err diff --git a/internal/cmd/remove.go b/internal/cmd/remove.go index b32c0970..deacf82b 100644 --- a/internal/cmd/remove.go +++ b/internal/cmd/remove.go @@ -4,24 +4,21 @@ import ( "bytes" "errors" "fmt" - "os/exec" + "os" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tcnksm/go-input" "gopkg.in/yaml.v3" "github.com/craftcms/nitro/config" - "github.com/craftcms/nitro/internal/nitro" "github.com/craftcms/nitro/internal/prompt" - "github.com/craftcms/nitro/internal/sudo" ) var removeCommand = &cobra.Command{ Use: "remove", Short: "Manage your nitro sites", RunE: func(cmd *cobra.Command, args []string) error { - machine := flagMachineName - var configFile config.Config if err := viper.Unmarshal(&configFile); err != nil { return err @@ -33,9 +30,17 @@ var removeCommand = &cobra.Command{ return errors.New("there are no sites to remove") } - i, _ := prompt.Select("Select site to remove", configFile.SitesAsList()) + ui := &input.UI{ + Writer: os.Stdout, + Reader: os.Stdin, + } - site := sites[i] + var site config.Site + _, i, err := prompt.Select(ui, "Select a site to remove:", sites[0].Hostname, configFile.SitesAsList()) + if err != nil { + return err + } + site = sites[i] // find the mount mount := configFile.FindMountBySiteWebroot(site.Webroot) @@ -59,9 +64,13 @@ var removeCommand = &cobra.Command{ if err := viper.ReadConfig(bytes.NewBuffer(c)); err != nil { return err } - if err := viper.WriteConfigAs(viper.ConfigFileUsed()); err != nil { - return err + + if !flagDebug { + if err := viper.WriteConfigAs(viper.ConfigFileUsed()); err != nil { + return err + } } + // unmarshal the messy config into a config var messyConfig config.Config if err := viper.Unmarshal(&messyConfig); err != nil { @@ -76,60 +85,16 @@ var removeCommand = &cobra.Command{ } // END HACK - actions, err := removeActions(machine, *mount, site) + applyChanges, err := prompt.Verify(ui, "Apply changes from config now?", "y") if err != nil { return err } - // save the config - if flagDebug { - for _, a := range actions { - fmt.Println(a.Args) - } - return nil + if applyChanges { + fmt.Println("Ok, applying changes from the config file...") + return applyCommand.RunE(cmd, args) } - if err := nitro.Run(nitro.NewMultipassRunner("multipass"), actions); err != nil { - fmt.Println("Failed to remove the site:", err) - return err - } - - fmt.Println("Removed the site from your config and applied the changes.") - - // prompt to remove hosts file - nitro, err := exec.LookPath("nitro") - if err != nil { - return err - } - - fmt.Println("Removing site from your hosts file") - - return sudo.RunCommand(nitro, machine, "hosts", "remove", site.Hostname) + return nil }, } - -func removeActions(name string, mount config.Mount, site config.Site) ([]nitro.Action, error) { - var actions []nitro.Action - - // unmount - unmountAction, err := nitro.UnmountDir(name, mount.Dest) - if err != nil { - return nil, err - } - actions = append(actions, *unmountAction) - - // remove nginx symlink - removeSymlinkAction, err := nitro.RemoveSymlink(name, site.Hostname) - if err != nil { - return nil, err - } - actions = append(actions, *removeSymlinkAction) - - restartNginxAction, err := nitro.NginxReload(name) - if err != nil { - return nil, err - } - actions = append(actions, *restartNginxAction) - - return actions, nil -} diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go new file mode 100644 index 00000000..55f21c18 --- /dev/null +++ b/internal/cmd/rename.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tcnksm/go-input" + + "github.com/craftcms/nitro/config" + "github.com/craftcms/nitro/internal/prompt" +) + +var renameCommand = &cobra.Command{ + Use: "rename", + Short: "Rename a site", + RunE: func(cmd *cobra.Command, args []string) error { + var configFile config.Config + if err := viper.Unmarshal(&configFile); err != nil { + return err + } + + sites := configFile.GetSites() + + if len(sites) == 0 { + return errors.New("there are no sites to rename") + } + + ui := &input.UI{ + Writer: os.Stdout, + Reader: os.Stdin, + } + + // ask to select a site + var site config.Site + _, i, err := prompt.Select(ui, "Select a site to rename:", sites[0].Hostname, configFile.SitesAsList()) + if err != nil { + return err + } + site = sites[i] + + // ask for the new newHostname + var newHostname string + newHostname, err = prompt.Ask(ui, "What should the new hostname be?", site.Hostname, true) + if err != nil { + return err + } + if site.Hostname == newHostname { + return errors.New("the new and original hostnames match, nothing to do") + } + + // update the config + if err := configFile.RenameSite(site, newHostname); err != nil { + return err + } + + // save the file + if !flagDebug { + if err := configFile.Save(viper.ConfigFileUsed()); err != nil { + return err + } + } + + applyChanges, err := prompt.Verify(ui, "Apply changes from config now?", "y") + if err != nil { + return err + } + + if applyChanges { + fmt.Println("Ok, applying changes from the config file...") + return applyCommand.RunE(cmd, args) + } + + return nil + }, +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index e7974eb7..dc91fe20 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -43,6 +43,7 @@ func init() { editCommand, importCommand, hostsCommand, + renameCommand, ) xdebugCommand.AddCommand(xdebugOnCommand, xdebugOffCommand, xdebugConfigureCommand) } diff --git a/internal/find/find.go b/internal/find/find.go index 105ddef9..d047d297 100644 --- a/internal/find/find.go +++ b/internal/find/find.go @@ -1,6 +1,7 @@ package find import ( + "bufio" "bytes" "encoding/csv" "fmt" @@ -10,6 +11,12 @@ import ( "github.com/craftcms/nitro/config" ) +// Finder is an interface the wraps the exec.Command Output function +// it is used by this package to parse output of the exec.Command +type Finder interface { + Output() ([]byte, error) +} + // Mounts will take a name of a machine and the output of an exec.Command as a slice of bytes // and return a slice of config mounts that has a source and destination or an error. This is // used to match if the machine has any mounts. The args passed to multipass are expected to @@ -42,6 +49,19 @@ func Mounts(name string, b []byte) ([]config.Mount, error) { return mounts, nil } +func ExistingContainer(f Finder, database config.Database) (*config.Database, error) { + output, err := f.Output() + if err != nil { + return nil, err + } + + if strings.Contains(string(output), "exists") { + return &database, nil + } + + return nil, nil +} + func ContainersToCreate(machine string, cfg config.Config) ([]config.Database, error) { path, err := exec.LookPath("multipass") if err != nil { @@ -50,9 +70,7 @@ func ContainersToCreate(machine string, cfg config.Config) ([]config.Database, e var dbs []config.Database for _, db := range cfg.Databases { - container := fmt.Sprintf("%s_%s_%s", db.Engine, db.Version, db.Port) - - c := exec.Command(path, []string{"exec", machine, "--", "sudo", "bash", "/opt/nitro/scripts/docker-container-exists.sh", container}...) + c := exec.Command(path, []string{"exec", machine, "--", "sudo", "bash", "/opt/nitro/scripts/docker-container-exists.sh", db.Name()}...) output, err := c.Output() if err != nil { return nil, err @@ -65,3 +83,80 @@ func ContainersToCreate(machine string, cfg config.Config) ([]config.Database, e return dbs, nil } + +// SitesEnabled takes a finder which is a command executed +// by the multipass cli tool that outputs the contents +// (symlinks) or sites-enabled and returns sites. +func SitesEnabled(f Finder) ([]config.Site, error) { + out, err := f.Output() + if err != nil { + return nil, err + } + + // parse the out + var sites []config.Site + sc := bufio.NewScanner(strings.NewReader(string(out))) + for sc.Scan() { + if l := sc.Text(); l != "" { + sp := strings.Split(strings.TrimSpace(sc.Text()), "/") + if h := sp[len(sp)-1]; h != "default" { + sites = append(sites, config.Site{Hostname: h}) + } + } + } + + return sites, nil +} + +// PHPVersion is used to get the "current" or "default" version +// of PHP that is installed. It expects exec.Command to sent +// "multipass", "exec", machine, "--", "php", "--version" +func PHPVersion(f Finder) (string, error) { + out, err := f.Output() + if err != nil { + return "", err + } + + var version string + sc := bufio.NewScanner(strings.NewReader(string(out))) + c := 0 + for sc.Scan() { + if c > 0 { + break + } + + if l := sc.Text(); l != "" { + sp := strings.Split(strings.TrimSpace(sc.Text()), " ") + full := strings.Split(sp[1], ".") + version = fmt.Sprintf("%s.%s", full[0], full[1]) + } + + c = c + 1 + } + + return version, nil +} + +// docker container ls --format '{{.Names}}' +func AllDatabases(f Finder) ([]config.Database, error) { + out, err := f.Output() + if err != nil { + return nil, err + } + + var databases []config.Database + sc := bufio.NewScanner(strings.NewReader(string(out))) + for sc.Scan() { + if strings.Contains(sc.Text(), "mysql") || strings.Contains(sc.Text(), "postgres") { + sp := strings.Split(sc.Text(), "_") + db := config.Database{ + Engine: strings.TrimLeft(sp[0], "'"), + Version: sp[1], + Port: strings.TrimRight(sp[2], "'"), + } + databases = append(databases, db) + } + } + + return databases, nil +} diff --git a/internal/nitro/docker.go b/internal/nitro/docker.go index 9ed4db51..0bbb762a 100644 --- a/internal/nitro/docker.go +++ b/internal/nitro/docker.go @@ -3,13 +3,14 @@ package nitro import ( "fmt" + "github.com/craftcms/nitro/config" "github.com/craftcms/nitro/validate" ) // CreateDatabaseContainer is responsible for the creation of a new Docker database and will // assign a volume and port based on the arguments. Validation of port collisions should occur // outside of this func and this will only validate engines and versions. -func CreateDatabaseContainer(name, engine, version, port string) (*Action, error) { +func CreateDatabaseContainer(machine, engine, version, port string) (*Action, error) { if err := validate.DatabaseEngineAndVersion(engine, version); err != nil { return nil, err } @@ -33,13 +34,13 @@ func CreateDatabaseContainer(name, engine, version, port string) (*Action, error volume := containerVolume(engine, version, port) volumeMount := fmt.Sprintf("%s:%s", volume, containerPath) - // build the container name based on engine, version, and port + // build the container machine based on engine, version, and port containerName := containerName(engine, version, port) // create the port mapping portMapping := fmt.Sprintf("%v:%v", port, containerPort) - args := []string{"exec", name, "--", "docker", "run", "-v", volumeMount, "--name", containerName, "-d", "--restart=always", "-p", portMapping} + args := []string{"exec", machine, "--", "docker", "run", "-v", volumeMount, "--name", containerName, "-d", "--restart=always", "-p", portMapping} // append the env vars args = append(args, containerEnvVars...) @@ -54,7 +55,17 @@ func CreateDatabaseContainer(name, engine, version, port string) (*Action, error }, nil } -func CreateDatabaseVolume(name, engine, version, port string) (*Action, error) { +// SetDatabaseUserPermissions is used to set all permissions on the nitro user for a database +func SetDatabaseUserPermissions(machine string, database config.Database) (*Action, error) { + return &Action{ + Type: "exec", + UseSyscall: false, + Args: []string{"exec", machine, "--", "sudo", "bash", "/opt/nitro/scripts/docker-set-database-user-permissions.sh", database.Name(), database.Engine}, + }, nil +} + +// CreateDatabaseVolume will make a database vaolume to ensure that data is persisted during reboots. +func CreateDatabaseVolume(machine, engine, version, port string) (*Action, error) { if err := validate.DatabaseEngineAndVersion(engine, version); err != nil { return nil, err } @@ -64,7 +75,7 @@ func CreateDatabaseVolume(name, engine, version, port string) (*Action, error) { return &Action{ Type: "exec", UseSyscall: false, - Args: []string{"exec", name, "--", "docker", "volume", "create", volume}, + Args: []string{"exec", machine, "--", "docker", "volume", "create", volume}, }, nil } diff --git a/internal/nitro/info.go b/internal/nitro/info.go index 851bb3cc..d846a12b 100644 --- a/internal/nitro/info.go +++ b/internal/nitro/info.go @@ -3,14 +3,14 @@ package nitro import "errors" // Info will display the machine information based on a name -func Info(name string) (*Action, error) { - if name == "" { +func Info(machine string) (*Action, error) { + if machine == "" { return nil, errors.New("missing machine name") } return &Action{ Type: "info", UseSyscall: false, - Args: []string{"info", name}, + Args: []string{"info", machine}, }, nil } diff --git a/internal/nitro/launch.go b/internal/nitro/launch.go index bd9f5ca2..8597f675 100644 --- a/internal/nitro/launch.go +++ b/internal/nitro/launch.go @@ -37,6 +37,6 @@ func Launch(name string, cpus int, memory, disk, input string) (*Action, error) Type: "launch", UseSyscall: false, Input: input, - Args: []string{"launch", "--name", name, "--cpus", strconv.Itoa(cpus), "--mem", memory, "--disk", disk, "--cloud-init", "-"}, + Args: []string{"launch", "--name", name, "--cpus", strconv.Itoa(cpus), "--mem", memory, "--disk", disk, "bionic", "--cloud-init", "-"}, }, nil } diff --git a/internal/nitro/launch_test.go b/internal/nitro/launch_test.go index 9305da86..9b3738dd 100644 --- a/internal/nitro/launch_test.go +++ b/internal/nitro/launch_test.go @@ -32,7 +32,7 @@ func TestLaunch(t *testing.T) { Type: "launch", UseSyscall: false, Input: "someinput", - Args: []string{"launch", "--name", "machine", "--cpus", "4", "--mem", "2G", "--disk", "20G", "--cloud-init", "-"}, + Args: []string{"launch", "--name", "machine", "--cpus", "4", "--mem", "2G", "--disk", "20G", "bionic", "--cloud-init", "-"}, }, wantErr: false, }, diff --git a/internal/nitro/redis.go b/internal/nitro/redis.go index 6e7fc859..2a5c05e5 100644 --- a/internal/nitro/redis.go +++ b/internal/nitro/redis.go @@ -1,15 +1,23 @@ package nitro -import "errors" +import ( + "errors" + "runtime" +) func Redis(name string) (*Action, error) { if name == "" { return nil, errors.New("name cannot be empty") } + syscall := true + if runtime.GOOS == "windows" { + syscall = false + } + return &Action{ Type: "exec", - UseSyscall: true, + UseSyscall: syscall, Args: []string{"exec", name, "--", "redis-cli"}, }, nil } diff --git a/internal/nitro/site_add.go b/internal/nitro/site_add.go index c3951561..d0eb8905 100644 --- a/internal/nitro/site_add.go +++ b/internal/nitro/site_add.go @@ -54,21 +54,19 @@ func ChangeTemplateVariables(name, webroot, hostname, php string, aliases []stri hostname = hostname + " " + strings.Join(aliases, " ") } - actions = append(actions, *changeVariables(name, template, "CHANGEWEBROOTDIR", webroot)) - actions = append(actions, *changeVariables(name, template, "CHANGESERVERNAME", hostname)) - actions = append(actions, *changeVariables(name, template, "CHANGEPHPVERSION", php)) + actions = append(actions, *ChangeNginxTemplateVariable(name, template, "CHANGEWEBROOTDIR", webroot)) + actions = append(actions, *ChangeNginxTemplateVariable(name, template, "CHANGESERVERNAME", hostname)) + actions = append(actions, *ChangeNginxTemplateVariable(name, template, "CHANGEPHPVERSION", php)) return &actions, nil } -func changeVariables(name, site, variable, actual string) *Action { - file := fmt.Sprintf("/etc/nginx/sites-available/%v", site) - +func ChangeNginxTemplateVariable(machine, hostname, variable, actual string) *Action { sedCmd := "s|" + variable + "|" + actual + "|g" return &Action{ Type: "exec", UseSyscall: false, - Args: []string{"exec", name, "--", "sudo", "sed", "-i", sedCmd, file}, + Args: []string{"exec", machine, "--", "sudo", "sed", "-i", sedCmd, fmt.Sprintf("/etc/nginx/sites-available/%v", hostname)}, } } diff --git a/internal/nitro/ssh.go b/internal/nitro/ssh.go index 4d667678..deb83c6d 100644 --- a/internal/nitro/ssh.go +++ b/internal/nitro/ssh.go @@ -1,15 +1,24 @@ package nitro -import "github.com/craftcms/nitro/validate" +import ( + "runtime" + + "github.com/craftcms/nitro/validate" +) func SSH(name string) (*Action, error) { if err := validate.MachineName(name); err != nil { return nil, err } + syscall := true + if runtime.GOOS == "windows" { + syscall = false + } + return &Action{ Type: "shell", - UseSyscall: true, + UseSyscall: syscall, Args: []string{"shell", name}, }, nil } diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 95966dfa..79386514 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -1,78 +1,57 @@ package prompt import ( - "github.com/manifoldco/promptui" + "errors" + "strings" + + "github.com/tcnksm/go-input" ) -func Ask(label, def string, validator promptui.ValidateFunc) (string, error) { - p := promptui.Prompt{ - Label: label, +func Ask(ui *input.UI, query, def string, req bool) (string, error) { + a, err := ui.Ask(query, &input.Options{ Default: def, - Validate: validator, - } - - v, err := p.Run() + Required: req, + Loop: true, + }) if err != nil { return "", err } - return v, nil + return a, nil } -func AskWithDefault(label, def string, validator promptui.ValidateFunc) (string, error) { - p := promptui.Prompt{ - Label: label + " [" + def + "]", - Validate: validator, - } - - v, err := p.Run() +// Select is responsible for providing a list of options to remove +func Select(ui *input.UI, query, def string, list []string) (string, int, error) { + selected, err := ui.Select(query, list, &input.Options{ + Required: true, + Default: def, + }) if err != nil { - return "", err - } - - switch v { - case "": - v = def - } - - return v, nil -} - -func SelectWithDefault(label, def string, options []string) (int, string) { - p := promptui.Select{ - Label: label + " [" + def + "]", - Items: options, + return "", 0, err } - i, selected, _ := p.Run() - - return i, selected -} - -func Select(label string, options []string) (int, string) { - p := promptui.Select{ - Label: label, - Items: options, + for i, s := range list { + if s == selected { + return s, i, nil + } } - i, selected, _ := p.Run() - - return i, selected + return "", 0, errors.New("unable to find the selected option") } -func Verify(label string) bool { - verify := promptui.Prompt{ - Label: label, - } - - answer, err := verify.Run() +func Verify(ui *input.UI, query, def string) (bool, error) { + a, err := ui.Ask(query, &input.Options{ + Default: def, + Required: true, + Loop: true, + }) if err != nil { - return false + return false, err } - if answer == "" { - return true + if strings.ContainsAny(a, "y") { + return true, nil } - return false + return false, nil } diff --git a/internal/task/apply.go b/internal/task/apply.go new file mode 100644 index 00000000..68258676 --- /dev/null +++ b/internal/task/apply.go @@ -0,0 +1,134 @@ +package task + +import ( + "github.com/craftcms/nitro/config" + "github.com/craftcms/nitro/internal/nitro" +) + +// Apply is responsible for comparing the current configuration and what information is +// found on a machine such as fromMultipassMounts and sites. Apple will then take the appropriate +// steps to compare are create actions that "normal up" the configuration state. +func Apply(machine string, configFile config.Config, mounts []config.Mount, sites []config.Site, dbs []config.Database, php string) ([]nitro.Action, error) { + var actions []nitro.Action + inMemoryConfig := config.Config{PHP: php, Mounts: mounts, Sites: sites, Databases: dbs} + + // check if there are mounts we need to remove + for _, mount := range inMemoryConfig.Mounts { + if !configFile.MountExists(mount.Dest) { + unmountAction, err := nitro.UnmountDir(machine, mount.Dest) + if err != nil { + return nil, err + } + actions = append(actions, *unmountAction) + } + } + + // check if there are mounts we need to create + for _, mount := range configFile.Mounts { + if !inMemoryConfig.MountExists(mount.Dest) { + mountAction, err := nitro.MountDir(machine, mount.AbsSourcePath(), mount.Dest) + if err != nil { + return nil, err + } + actions = append(actions, *mountAction) + } + } + + // check if there are sites we need to remove + for _, site := range inMemoryConfig.Sites { + if !configFile.SiteExists(site) { + // remove symlink + removeSymlink, err := nitro.RemoveSymlink(machine, site.Hostname) + if err != nil { + return nil, err + } + actions = append(actions, *removeSymlink) + + // reload nginx + reloadNginxAction, err := nitro.NginxReload(machine) + if err != nil { + return nil, err + } + actions = append(actions, *reloadNginxAction) + } + } + + // check if there are sites we need to make + for _, site := range configFile.Sites { + // find the parent to mount + if !inMemoryConfig.SiteExists(site) { + // copy template + copyTemplateAction, err := nitro.CopyNginxTemplate(machine, site.Hostname) + if err != nil { + return nil, err + } + actions = append(actions, *copyTemplateAction) + + // replace variable + changeNginxVariablesAction, err := nitro.ChangeTemplateVariables(machine, site.Webroot, site.Hostname, configFile.PHP, site.Aliases) + if err != nil { + return nil, err + } + actions = append(actions, *changeNginxVariablesAction...) + + createSymlink, err := nitro.CreateSiteSymllink(machine, site.Hostname) + if err != nil { + return nil, err + } + actions = append(actions, *createSymlink) + + // reload nginx + reloadNginxAction, err := nitro.NginxReload(machine) + if err != nil { + return nil, err + } + actions = append(actions, *reloadNginxAction) + } + } + + // check if there are databases to remove + for _, database := range inMemoryConfig.Databases { + if !configFile.DatabaseExists(database) { + actions = append(actions, nitro.Action{ + Type: "exec", + UseSyscall: false, + Args: []string{"exec", machine, "--", "docker", "rm", "-v", database.Name(), "-f"}, + }) + } + } + + // check if there are database to create + for _, database := range configFile.Databases { + if !inMemoryConfig.DatabaseExists(database) { + createVolume, err := nitro.CreateDatabaseVolume(machine, database.Engine, database.Version, database.Port) + if err != nil { + return nil, err + } + actions = append(actions, *createVolume) + + createContainer, err := nitro.CreateDatabaseContainer(machine, database.Engine, database.Version, database.Port) + if err != nil { + return nil, err + } + actions = append(actions, *createContainer) + + setUserPermissions, err := nitro.SetDatabaseUserPermissions(machine, database) + if err != nil { + return nil, err + } + actions = append(actions, *setUserPermissions) + } + } + + // if the php versions do not match, install the requested version - which makes it the default + if configFile.PHP != php { + installPhp, err := nitro.InstallPackages(machine, configFile.PHP) + if err != nil { + return nil, err + } + actions = append(actions, *installPhp) + + } + + return actions, nil +} diff --git a/internal/task/apply_test.go b/internal/task/apply_test.go new file mode 100644 index 00000000..3c63e10a --- /dev/null +++ b/internal/task/apply_test.go @@ -0,0 +1,451 @@ +package task + +import ( + "reflect" + "testing" + + "github.com/craftcms/nitro/config" + "github.com/craftcms/nitro/internal/nitro" +) + +func TestApply(t *testing.T) { + type args struct { + machine string + configFile config.Config + fromMultipassMounts []config.Mount + sites []config.Site + dbs []config.Database + php string + } + tests := []struct { + name string + args args + want []nitro.Action + wantErr bool + }{ + { + name: "mismatched versions of PHP installs the request version from the config file", + args: args{ + machine: "mytestmachine", + configFile: config.Config{PHP: "7.4"}, + php: "7.2", + }, + want: []nitro.Action{ + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "apt-get", "install", "-y", "php7.4", "php7.4-mbstring", "php7.4-cli", "php7.4-curl", "php7.4-fpm", "php7.4-gd", "php7.4-intl", "php7.4-json", "php7.4-mysql", "php7.4-opcache", "php7.4-pgsql", "php7.4-zip", "php7.4-xml", "php7.4-soap", "php-xdebug", "php-imagick", "blackfire-agent", "blackfire-php"}, + }, + }, + }, + { + name: "new databases that are in the config are created", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + Databases: []config.Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + }, + }, + dbs: nil, + }, + want: []nitro.Action{ + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "docker", "volume", "create", "mysql_5.7_3306"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "docker", "run", "-v", "mysql_5.7_3306:/var/lib/mysql", "--name", "mysql_5.7_3306", "-d", "--restart=always", "-p", "3306:3306", "-e", "MYSQL_ROOT_PASSWORD=nitro", "-e", "MYSQL_USER=nitro", "-e", "MYSQL_PASSWORD=nitro", "mysql:5.7"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "bash", "/opt/nitro/scripts/docker-set-database-user-permissions.sh", "mysql_5.7_3306", "mysql"}, + }, + }, + }, + { + name: "new databases are created but the ones in the config are kept", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + Databases: []config.Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + { + Engine: "postgres", + Version: "11", + Port: "5432", + }, + }, + }, + dbs: []config.Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + { + Engine: "postgres", + Version: "11", + Port: "5432", + }, + { + Engine: "postgres", + Version: "12", + Port: "54321", + }, + }, + }, + want: []nitro.Action{ + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "docker", "rm", "-v", "postgres_12_54321", "-f"}, + }, + }, + }, + { + name: "databases that are not in the config are removed", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + Databases: []config.Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + }, + }, + dbs: []config.Database{ + { + Engine: "mysql", + Version: "5.7", + Port: "3306", + }, + { + Engine: "postgres", + Version: "11", + Port: "5432", + }, + }, + }, + want: []nitro.Action{ + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "docker", "rm", "-v", "postgres_11_5432", "-f"}, + }, + }, + }, + { + name: "sites that exist but are not in the config file are removed", + args: args{ + machine: "mytestmachine", + configFile: config.Config{}, + fromMultipassMounts: []config.Mount{ + { + Source: "./testdata/existing/mount", + Dest: "/nitro/sites/leftoversite.test", + }, + }, + sites: []config.Site{ + { + Hostname: "leftoversite.test", + Webroot: "/nitro/sites/leftoversite.test/web", + }, + }, + }, + want: []nitro.Action{ + { + Type: "umount", + UseSyscall: false, + Args: []string{"umount", "mytestmachine:/nitro/sites/leftoversite.test"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "rm", "/etc/nginx/sites-enabled/leftoversite.test"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "service", "nginx", "restart"}, + }, + }, + }, + { + name: "new sites without a parent mount in the config are added to the machine and mounted", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + PHP: "7.4", + Mounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/existing-site", + }, + }, + Sites: []config.Site{ + { + Hostname: "existing-site", + Webroot: "/nitro/sites/existing-site", + }, + { + Hostname: "new-site", + Webroot: "/nitro/sites/new-site", + }, + }, + }, + fromMultipassMounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/existing-site", + }, + }, + sites: []config.Site{ + { + Hostname: "existing-site", + Webroot: "/nitro/sites/existing-site", + }, + }, + php: "7.4", + }, + want: []nitro.Action{ + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "cp", "/opt/nitro/nginx/template.conf", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "sed", "-i", "s|CHANGEWEBROOTDIR|/nitro/sites/new-site|g", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "sed", "-i", "s|CHANGESERVERNAME|new-site|g", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "sed", "-i", "s|CHANGEPHPVERSION|7.4|g", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "ln", "-s", "/etc/nginx/sites-available/new-site", "/etc/nginx/sites-enabled/"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "service", "nginx", "restart"}, + }, + }, + wantErr: false, + }, + { + name: "new sites using parent mounts in the config are added to the machine", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + PHP: "7.4", + Mounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites", + }, + }, + Sites: []config.Site{ + { + Hostname: "existing-site", + Webroot: "/nitro/sites/existing-site", + }, + { + Hostname: "new-site", + Webroot: "/nitro/sites/new-site", + }, + }, + }, + fromMultipassMounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites", + }, + }, + sites: []config.Site{ + { + Hostname: "existing-site", + Webroot: "/nitro/sites/existing-site", + }, + }, + php: "7.4", + }, + want: []nitro.Action{ + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "cp", "/opt/nitro/nginx/template.conf", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "sed", "-i", "s|CHANGEWEBROOTDIR|/nitro/sites/new-site|g", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "sed", "-i", "s|CHANGESERVERNAME|new-site|g", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "sed", "-i", "s|CHANGEPHPVERSION|7.4|g", "/etc/nginx/sites-available/new-site"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "ln", "-s", "/etc/nginx/sites-available/new-site", "/etc/nginx/sites-enabled/"}, + }, + { + Type: "exec", + UseSyscall: false, + Args: []string{"exec", "mytestmachine", "--", "sudo", "service", "nginx", "restart"}, + }, + }, + wantErr: false, + }, + { + name: "new mounts return actions to create mounts", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + Mounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/example-site", + }, + { + Source: "./testdata/new-mount", + Dest: "/nitro/sites/new-site", + }, + }, + }, + fromMultipassMounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/example-site", + }, + }, + }, + want: []nitro.Action{ + { + Type: "mount", + UseSyscall: false, + Args: []string{"mount", "./testdata/new-mount", "mytestmachine:/nitro/sites/new-site"}, + }, + }, + wantErr: false, + }, + { + name: "removed mounts return actions to remove mounts", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + Mounts: []config.Mount{ + { + Source: "./testdata/new-mount", + Dest: "/nitro/sites/new-site", + }, + }, + }, + fromMultipassMounts: []config.Mount{ + { + Source: "./testdata/new-mount", + Dest: "/nitro/sites/new-site", + }, + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/example-site", + }, + }, + php: "", + }, + want: []nitro.Action{ + { + Type: "umount", + UseSyscall: false, + Args: []string{"umount", "mytestmachine:/nitro/sites/example-site"}, + }, + }, + wantErr: false, + }, + { + name: "renamed mounts get removed and added", + args: args{ + machine: "mytestmachine", + configFile: config.Config{ + Mounts: []config.Mount{ + { + Source: "./testdata/new-mount", + Dest: "/nitro/sites/new-site", + }, + }, + }, + fromMultipassMounts: []config.Mount{ + { + Source: "./testdata/existing-mount", + Dest: "/nitro/sites/existing-site", + }, + }, + }, + want: []nitro.Action{ + { + Type: "umount", + UseSyscall: false, + Args: []string{"umount", "mytestmachine:/nitro/sites/existing-site"}, + }, + { + Type: "mount", + UseSyscall: false, + Args: []string{"mount", "./testdata/new-mount", "mytestmachine:/nitro/sites/new-site"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Apply(tt.args.machine, tt.args.configFile, tt.args.fromMultipassMounts, tt.args.sites, tt.args.dbs, tt.args.php) + if (err != nil) != tt.wantErr { + t.Errorf("Apply() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if len(tt.want) != len(got) { + t.Errorf("expected the number of actions to be equal for Apply(); got %d, want %d", len(got), len(tt.want)) + return + } + + if tt.want != nil { + for i, action := range tt.want { + if !reflect.DeepEqual(action, got[i]) { + t.Errorf("Apply() got = \n%v, \nwant \n%v", got[i], tt.want[i]) + } + } + } + }) + } +} diff --git a/internal/task/testdata/existing-mount/.gitkeep b/internal/task/testdata/existing-mount/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/internal/task/testdata/new-mount/.gitkeep b/internal/task/testdata/new-mount/.gitkeep new file mode 100644 index 00000000..e69de29b