Commit 1c189970 authored by Addshore's avatar Addshore 🏄 Committed by Addshore
Browse files

Refactor internal mediawiki code to a type

This is further prep in the direction of
I7d1a66dd96b293d3fe3d6b371212e34018872a49
that is useful in any case.

The internal/mediawiki package now exposes a MediaWiki type
which has the various methods attached for interacting
with a MediaWiki directory.

The internal/docker package contains functionality of the mediawiki-docker
dev environment.

This means there is now a clear split between the CLI / user / presentation
layer, which exists in the cmd package.
The buisness layer that lives in other packages.
internal/mediawiki: things generically performed to mediawiki on disk
internal/docker: things specifica to the mediawiki-docker command / environment

Change-Id: I487866628a87d11eeb4d467d5c718a706f7c6e23
parent 1721d16c
......@@ -21,6 +21,14 @@ Within the `~/go/src/gerrit.wikimedia.org/r/mediawiki/tools/cli/cmd` directory:
Execute the script from any directory with `go run ~/go/src/gerrit.wikimedia.org/r/mediawiki/tools/cli/cmd/cli/main.go`
### Packages
- `cmd`: Contains the Cobra commands and deals with all CLI user interaction
- `internal/docker`: Logic interacting with the mediawiki-docker dev environment
- `internal/env`: Logic interacting with a `.env` file
- `internal/exec`: Wrapper for the main `exec` package, providing easy verbosity etc
- `internal/mediawiki`: Logic interacting with a MediaWiki directory on disk
### Using a binary
Make a binary by running `make install`
......
......@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"github.com/manifoldco/promptui"
"bytes"
"fmt"
"log"
......@@ -28,6 +29,7 @@ import (
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/docker"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/exec"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/mediawiki"
)
......@@ -48,6 +50,15 @@ var dockerCmd = &cobra.Command{
RunE: nil,
}
func mediawikiOrFatal() mediawiki.MediaWiki {
MediaWiki, err := mediawiki.ForCurrentWorkingDirectory()
if err != nil {
log.Fatal("❌ Please run this command within the root of the MediaWiki core repository.")
os.Exit(1);
}
return MediaWiki
}
var startCmd = &cobra.Command{
Use: "start",
Short: "Start the development environment",
......@@ -60,41 +71,88 @@ var startCmd = &cobra.Command{
Verbosity: Verbosity,
HandleError: handlePortError,
}
exec.RunCommand(options, exec.DockerComposeCommand("up", "-d"))
MediaWiki := mediawikiOrFatal()
mediawiki.InitialSetup(exec.HandlerOptions{
Verbosity: Verbosity,
})
exec.RunCommand(options, exec.DockerComposeCommand("up", "-d"))
printSuccess()
},
PreRun: func(cmd *cobra.Command, args []string) {
mediawiki.CheckIfInCoreDirectory()
if isLinuxHost() {
// TODO: We should also check the contents for correctness, maybe
// using docker-compose config and asserting that UID/GID mapping is present
// and with correct values.
_, err := os.Stat("docker-compose.override.yml")
if err != nil {
fileCreated,err := docker.EnsureDockerComposeUserOverrideExists()
if fileCreated {
fmt.Println("Creating docker-compose.override.yml for correct user ID and group ID mapping from host to container")
var data = `
version: '3.7'
services:
mediawiki:
user: "${MW_DOCKER_UID}:${MW_DOCKER_GID}"
`
file, err := os.Create("docker-compose.override.yml")
if err != nil {
log.Fatal(err)
}
if err != nil {
log.Fatal(err)
}
}
MediaWiki.EnsureCacheDirectory()
if docker.MediaWikiComposerDependenciesNeedInstallation(exec.HandlerOptions{Verbosity: Verbosity}) {
fmt.Println("MediaWiki has some external dependencies that need to be installed")
prompt := promptui.Prompt{
IsConfirm: true,
Label: "Install dependencies now",
}
_, err := prompt.Run()
if err == nil {
Spinner := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
Spinner.Prefix = "Installing Composer dependencies (this may take a few minutes) "
Spinner.FinalMSG = Spinner.Prefix + "(done)\n"
options := exec.HandlerOptions{
Spinner: Spinner,
Verbosity: Verbosity,
}
docker.MediaWikiComposerUpdate(options)
}
}
if !MediaWiki.VectorIsPresent() {
prompt := promptui.Prompt{
IsConfirm: true,
Label: "Download and use the Vector skin",
}
_, err := prompt.Run()
if err == nil {
Spinner := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
Spinner.Prefix = "Downloading Vector "
Spinner.FinalMSG = Spinner.Prefix + "(done)\n"
options := exec.HandlerOptions{
Spinner: Spinner,
Verbosity: Verbosity,
HandleError: func(stderr bytes.Buffer, err error) {
if err != nil {
log.Fatal(err)
}
},
}
defer file.Close()
_, err = file.WriteString(data)
if err != nil {
log.Fatal(err)
MediaWiki.GitCloneVector(options)
}
}
if !MediaWiki.LocalSettingsIsPresent() {
prompt := promptui.Prompt{
IsConfirm: true,
Label: "Install MediaWiki database tables and create LocalSettings.php",
}
_, err := prompt.Run()
if err == nil {
Spinner := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
Spinner.Prefix = "Installing "
Spinner.FinalMSG = Spinner.Prefix + "(done)\n"
options := exec.HandlerOptions{
Spinner: Spinner,
Verbosity: Verbosity,
}
file.Sync()
docker.MediaWikiInstall(options)
}
}
printSuccess()
},
}
......@@ -102,6 +160,9 @@ var execCmd = &cobra.Command{
Use: "exec [service] [command] [args]",
Short: "Run a command in the specified container",
Args: cobra.MinimumNArgs(2),
PreRun: func(cmd *cobra.Command, args []string) {
mediawiki.CheckIfInCoreDirectory()
},
Run: func(cmd *cobra.Command, args []string) {
options := exec.HandlerOptions{
Verbosity: Verbosity,
......@@ -144,10 +205,9 @@ var execCmd = &cobra.Command{
var destroyCmd = &cobra.Command{
Use: "destroy [service...]",
Short: "destroys the development environment or specified containers",
PreRun: func(cmd *cobra.Command, args []string) {
mediawiki.CheckIfInCoreDirectory()
},
Run: func(cmd *cobra.Command, args []string) {
MediaWiki := mediawikiOrFatal()
options := exec.HandlerOptions{
Verbosity: Verbosity,
}
......@@ -156,9 +216,9 @@ var destroyCmd = &cobra.Command{
exec.RunTTYCommand(options, exec.DockerComposeCommand("rm", runArgs...))
if len(args) == 0 || contains(args, "mediawiki") {
mediawiki.RenameLocalSettings()
mediawiki.DeleteCache()
mediawiki.DeleteVendor()
MediaWiki.RenameLocalSettings()
MediaWiki.DeleteCache()
MediaWiki.DeleteVendor()
}
},
}
......
......@@ -21,7 +21,6 @@ import (
"fmt"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/env"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/mediawiki"
"github.com/spf13/cobra"
)
......@@ -30,9 +29,6 @@ var envCmd = &cobra.Command{
Use: "env",
Short: "Provides subcommands for interacting with development environment variables",
RunE: nil,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
mediawiki.CheckIfInCoreDirectory()
},
}
var deleteCmd = &cobra.Command{
......@@ -40,7 +36,7 @@ var deleteCmd = &cobra.Command{
Short: "Deletes an environment variable",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
env.DotFileForDirectory(mediawiki.Directory()).Delete(args[0])
env.DotFileForDirectory(mediawikiOrFatal().Directory()).Delete(args[0])
},
}
......@@ -49,7 +45,7 @@ var setCmd = &cobra.Command{
Short: "Set an environment variable",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
env.DotFileForDirectory(mediawiki.Directory()).Set(args[0], args[1])
env.DotFileForDirectory(mediawikiOrFatal().Directory()).Set(args[0], args[1])
},
}
......@@ -58,7 +54,7 @@ var getCmd = &cobra.Command{
Short: "Get an environment variable",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(env.DotFileForDirectory(mediawiki.Directory()).Get(args[0]))
fmt.Println(env.DotFileForDirectory(mediawikiOrFatal().Directory()).Get(args[0]))
},
}
......@@ -66,7 +62,7 @@ var listCmd = &cobra.Command{
Use: "list",
Short: "List all environment variables",
Run: func(cmd *cobra.Command, args []string) {
for name, value := range env.DotFileForDirectory(mediawiki.Directory()).List() {
for name, value := range env.DotFileForDirectory(mediawikiOrFatal().Directory()).List() {
fmt.Println(name + "=" + value)
}
},
......@@ -76,7 +72,7 @@ var whereCmd = &cobra.Command{
Use: "where",
Short: "Output the location of the .env file",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(env.DotFileForDirectory(mediawiki.Directory()).Path())
fmt.Println(env.DotFileForDirectory(mediawikiOrFatal().Directory()).Path())
},
}
......
/*Package docker is used to interact with docker development environment services
Copyright © 2020 Addshore
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package docker
import (
"os"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/exec"
)
/*MediaWikiInstall ...*/
func MediaWikiInstall( options exec.HandlerOptions ) {
exec.RunCommand(
options,
exec.DockerComposeCommand(
"exec",
"-T",
"mediawiki",
"/bin/bash",
"/docker/install.sh",
))
}
/*MediaWikiComposerUpdate ...*/
func MediaWikiComposerUpdate( options exec.HandlerOptions ) {
exec.RunCommand(
options,
exec.DockerComposeCommand(
"exec",
"-T",
"mediawiki",
"composer",
"update",
))
}
func mediaWikiPHPVersionCheck( options exec.HandlerOptions ) error {
return exec.RunCommand(options,
exec.DockerComposeCommand(
"exec",
"-T",
"mediawiki",
"php",
"-r",
"require_once dirname( __FILE__ ) . '/includes/PHPVersionCheck.php'; $phpVersionCheck = new PHPVersionCheck(); $phpVersionCheck->checkVendorExistence();",
))
}
/*MediaWikiComposerDependenciesNeedInstallation ...*/
func MediaWikiComposerDependenciesNeedInstallation(options exec.HandlerOptions) bool {
err := mediaWikiPHPVersionCheck(options)
return err != nil
}
/*EnsureDockerComposeUserOverrideExists Ensures that a docker-compose.override files exists with a mediawiki user and gid override*/
func EnsureDockerComposeUserOverrideExists() (bool, error){
// TODO: We should also check the contents for correctness, maybe
// using docker-compose config and asserting that UID/GID mapping is present
// and with correct values.
_, err := os.Stat("docker-compose.override.yml")
if err != nil {
var data = `
version: '3.7'
services:
mediawiki:
user: "${MW_DOCKER_UID}:${MW_DOCKER_GID}"
`
file, err := os.Create("docker-compose.override.yml")
if err != nil {
return false, err
}
defer file.Close()
_, err = file.WriteString(data)
if err != nil {
return false, err
}
file.Sync()
return true, nil;
}
return false, nil;
}
\ No newline at end of file
......@@ -18,163 +18,85 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package mediawiki
import (
"bytes"
"time"
"strings"
"io/ioutil"
"fmt"
"os"
"gerrit.wikimedia.org/r/mediawiki/tools/cli/internal/exec"
"time"
"github.com/briandowns/spinner"
"github.com/manifoldco/promptui"
"log"
)
/*InitialSetup tba*/
func InitialSetup( options exec.HandlerOptions) {
CheckIfInCoreDirectory()
makeCacheDirectory()
/*MediaWiki representation of a MediaWiki install directory*/
type MediaWiki string
if composerDependenciesNeedInstallation(options) {
promptToInstallComposerDependencies(options)
}
if !vectorIsPresent() {
promptToCloneVector(options)
}
if !localSettingsIsPresent() {
promptToInstallMediaWiki(options)
}
/*NotMediaWikiDirectory error when a directory appears to not contain MediaWiki code*/
type NotMediaWikiDirectory struct {
directory string
}
func (e *NotMediaWikiDirectory) Error() string {
return e.directory + " doesn't look like a MediaWiki directory"
}
/*Directory the current working directory if it looks like a MediaWiki directory*/
func Directory() string {
CheckIfInCoreDirectory()
/*ForCurrentWorkingDirectory returns a MediaWiki for the current working directory*/
func ForCurrentWorkingDirectory() (MediaWiki, error) {
currentWorkingDirectory, _ := os.Getwd()
return currentWorkingDirectory
return MediaWiki(currentWorkingDirectory), errorIfDirectoryDoesNotLookLikeCore(currentWorkingDirectory)
}
/*CheckIfInCoreDirectory checks that the current working directory looks like a MediaWiki directory*/
func CheckIfInCoreDirectory() {
b, err := ioutil.ReadFile(".gitreview")
if err != nil || !strings.Contains(string(b), "project=mediawiki/core.git") {
_, err := ForCurrentWorkingDirectory()
if err != nil {
log.Fatal("❌ Please run this command within the root of the MediaWiki core repository.")
}
}
func makeCacheDirectory() {
err := os.MkdirAll("cache", 0700)
if err != nil {
log.Fatal(err)
func errorIfDirectoryDoesNotLookLikeCore(directory string) error {
b, err := ioutil.ReadFile(directory + string(os.PathSeparator) + ".gitreview")
if err != nil || !strings.Contains(string(b), "project=mediawiki/core.git") {
return &NotMediaWikiDirectory{directory}
}
return nil
}
func composerDependenciesNeedInstallation(options exec.HandlerOptions) bool {
// Detect if composer dependencies are not installed and prompt user to install
err := exec.RunCommand(options,
exec.DockerComposeCommand(
"exec",
"-T",
"mediawiki",
"php",
"-r",
"require_once dirname( __FILE__ ) . '/includes/PHPVersionCheck.php'; $phpVersionCheck = new PHPVersionCheck(); $phpVersionCheck->checkVendorExistence();",
))
return err != nil
/*Directory the directory containing MediaWiki*/
func (m MediaWiki) Directory() string {
return string(m)
}
func promptToInstallComposerDependencies(options exec.HandlerOptions) {
fmt.Println("MediaWiki has some external dependencies that need to be installed")
prompt := promptui.Prompt{
IsConfirm: true,
Label: "Install dependencies now",
}
_, err := prompt.Run()
if err == nil {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Prefix = "Installing Composer dependencies (this may take a few minutes) "
s.FinalMSG = s.Prefix + "(done)\n"
options := exec.HandlerOptions{
Spinner: s,
Verbosity: options.Verbosity,
}
exec.RunCommand(options,
exec.DockerComposeCommand(
"exec",
"-T",
"mediawiki",
"composer",
"update",
))
func (m MediaWiki) path(subPath string) string {
return m.Directory() + string(os.PathSeparator) + subPath
}
/*EnsureCacheDirectory ...*/
func (m MediaWiki) EnsureCacheDirectory() {
err := os.MkdirAll("cache", 0700)
if err != nil {
log.Fatal(err)
}
}
func vectorIsPresent() bool {
info, err := os.Stat("skins/Vector")
/*VectorIsPresent ...*/
func (m MediaWiki) VectorIsPresent() bool {
info, err := os.Stat(m.path("skins/Vector"))
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}
func promptToCloneVector(options exec.HandlerOptions) {
prompt := promptui.Prompt{
IsConfirm: true,
Label: "Download and use the Vector skin",
}
_, err := prompt.Run()
if err == nil {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Prefix = "Downloading Vector "
s.FinalMSG = s.Prefix + "(done)\n"
options := exec.HandlerOptions{
Spinner: s,
Verbosity: options.Verbosity,
HandleError: func(stderr bytes.Buffer, err error) {
if err != nil {
log.Fatal(err)
}
},
}
exec.RunCommand(options, exec.Command(
"git",
"clone",
"https://gerrit.wikimedia.org/r/mediawiki/skins/Vector",
"skins/Vector"))
}
}
func promptToInstallMediaWiki(options exec.HandlerOptions) {
prompt := promptui.Prompt{
IsConfirm: true,
Label: "Install MediaWiki database tables and create LocalSettings.php",
}
_, err := prompt.Run()
if err == nil {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Prefix = "Installing "
s.FinalMSG = s.Prefix + "(done)\n"
options := exec.HandlerOptions{
Spinner: s,
Verbosity: options.Verbosity,
}
exec.RunCommand(
options,
exec.DockerComposeCommand(
"exec",
"-T",
"mediawiki",
"/bin/bash",
"/docker/install.sh"))
}
/*GitCloneVector ...*/
func (m MediaWiki) GitCloneVector(options exec.HandlerOptions) {
exec.RunCommand(options, exec.Command(
"git",
"clone",
"https://gerrit.wikimedia.org/r/mediawiki/skins/Vector",
m.path("skins/Vector")))
}
func localSettingsIsPresent() bool {
info, err := os.Stat("LocalSettings.php")
/*LocalSettingsIsPresent ...*/
func (m MediaWiki) LocalSettingsIsPresent() bool {
info, err := os.Stat(m.path("LocalSettings.php"))
if os.IsNotExist(err) {
return false
}
......@@ -182,14 +104,9 @@ func localSettingsIsPresent() bool {
}
/*RenameLocalSettings ...*/
func RenameLocalSettings() {
func (m MediaWiki) RenameLocalSettings() {
const layout = "2006-01-02T15:04:05-0700"
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Prefix = "Renaming LocalSettings file "
s.FinalMSG = s.Prefix + "(done)\n"
s.Start()
t := time.Now()
localSettingsName := "LocalSettings-" + t.Format(layout) + ".php"
......@@ -198,19 +115,11 @@ func RenameLocalSettings() {
if err != nil {
log.Fatal(err)
}
s.Stop()
}
/*DeleteCache ...*/
func DeleteCache() {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Prefix = "Deleting cache files "
s.FinalMSG = s.Prefix + "(done)\n"
s.Start()
err := os.Rename("cache/.htaccess", ".htaccess")
func (m MediaWiki) DeleteCache() {
err := os.Rename("cache/.htaccess", ".htaccess.fromcache.tmp")
if err != nil {
log.Fatal(err)
}
......@@ -225,22 +134,14 @@ func DeleteCache() {
log.Fatal(err)
}
err = os.Rename(".htaccess", "cache/.htaccess")
err = os.Rename(".htaccess.fromcache.tmp", "cache/.htaccess")
if err != nil {
log.Fatal(err)
}
s.Stop()
}
/*DeleteVendor ...*/
func DeleteVendor() {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Prefix = "Deleting vendor files "
s.FinalMSG = s.Prefix + "(done)\n"
s.Start()
func (m MediaWiki) DeleteVendor() {
err := os.RemoveAll("./vendor")
if err != nil {
log.Fatal(err)
......@@ -250,6 +151,4 @@ func DeleteVendor() {
if err != nil {