From 2c2523e655c4fac2638bea4b2bfc3a59a44af29e Mon Sep 17 00:00:00 2001 From: Edward Teach Date: Wed, 5 Dec 2018 23:30:50 -0800 Subject: [PATCH] initial commit --- CHANGELOG.md | 10 + CODE_OF_CONDUCT.md | 46 +++ LICENSE.md | 19 ++ app/src/Dappurware/Deployment.php | 447 ++++++++++++++++++++++++++++++ composer.json | 24 ++ 5 files changed, 546 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.md create mode 100644 app/src/Dappurware/Deployment.php create mode 100644 composer.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7dd177e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## [Unreleased] + +## [4.0.0] +### Notes +I am seperating the larger dapurware classes into their own packages as I would liek to develop them out a little more. + +[Unreleased]: https://github.com/dappur/dappurware-deployment/compare/v4.0.0...HEAD +[4.0.0]: https://github.com/dappur/dappurware-deployment/releases/tag/v4.0.0 \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..866837b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@dappur.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4974808 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2018 Dappur.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/app/src/Dappurware/Deployment.php b/app/src/Dappurware/Deployment.php new file mode 100644 index 0000000..de6356c --- /dev/null +++ b/app/src/Dappurware/Deployment.php @@ -0,0 +1,447 @@ +documentRoot = $_SERVER['DOCUMENT_ROOT']; + if (!is_null($documentRoot)) { + $this->documentRoot = $documentRoot; + } + $this->userHome = $_SERVER['HOME']; + if (!is_null($userHome)) { + $this->userHome = $userHome; + } + $this->gitBinPath = $gitBinPath; + $this->logFile = dirname($this->documentRoot) . '/storage/log/deployment/deployment-'.date("YmdGis").'.log'; + $this->repoDir = dirname($this->documentRoot) . '/repo'; + $this->certFolder = dirname($this->documentRoot) . '/storage/certs/deployment'; + $this->certFileName = $certFileName; + $this->certPath = realpath($this->certFolder . "/" . $this->certFileName); + set_time_limit($timeLimit); + + // Check for settings.json + $settingsFile = json_decode(file_get_contents(realpath($documentRoot) . "/../settings.json")); + if (!$settingsFile) { + die($this->logEntry("Could not locate a settings.json file in the document root.")); + } + $this->settings = $settingsFile; + + // Check Repo Url + if (!isset($settingsFile->deployment->repo_url) || $settingsFile->deployment->repo_url == "") { + die($this->logEntry("Please ensure that you have a repo URL in the deployment section of settings.json.")); + } + $this->repoUrl = $settingsFile->deployment->repo_url; + + // Check Repo Branch + if (!isset($settingsFile->deployment->repo_branch) || $settingsFile->deployment->repo_branch == "") { + die($this->logEntry("Please ensure that you have a repo branch in the deployment section of settings.json.")); + } + $this->repoBranch = $settingsFile->deployment->repo_branch; + + // Check Project Name + if (!isset($settingsFile->framework) || $settingsFile->framework == "") { + die($this->logEntry("Please ensure that you have a valid framework name in settings.json.")); + } + } + + // Initialize Dappur + public function initDappur() + { + echo $this->logEntry("Initializing Dappur Framework..."); + $this->validateDocumentRoot(); + $this->installUpdateComposer(); + $this->checkGit(); + $this->checkInstallRepo(); + $this->checkPhinx(); + $this->migrateUp(); + echo $this->logEntry("Framework initialization complete."); + } + + // Execute + public function execute() + { + echo $this->logEntry("Checking requirements..."); + $this->validateDocumentRoot(); + $this->installUpdateComposer(); + $this->checkGit(); + $this->checkInstallRepo(); + $this->checkConnection(); + echo $this->logEntry("Requirements validated!"); + } + + public function migrate() + { + echo $this->logEntry("Beginning Migration..."); + $this->checkPhinx(); + $this->migrateUp(); + } + + // Make sure PHP user has all appropriate permissions and that server + // structure is correct + private function validateDocumentRoot() + { + + // Check that the DOCUMENT_ROOT is a directory called `public`. + if (end(explode('\\', $this->documentRoot)) != "public" && + end(explode('/', $this->documentRoot)) != "public") { + die($this->logEntry("Your servers DOCUMENT_ROOT needs to be a directory called `public`")); + } + + // Check that the DOCUMENT_ROOT parent directory is writable. + if (!is_writable(dirname($this->documentRoot))) { + die( + $this->logEntry( + "Web server user does not have access to the DOCUMENT_ROOT's parent directory. ". + "This is required in order for Dappur to function properly." + ) + ); + } + } + + // Check if composer is installed or download phar and use that. + private function installUpdateComposer() + { + if (!is_file(dirname($this->documentRoot) . '/composer.phar')) { + // Download composer to the DOCUMENT_ROOT's parent directory. + if (file_put_contents( + dirname($this->documentRoot) . '/composer.phar', + fopen("https://getcomposer.org/download/1.7.1/composer.phar", 'r') + )) { + echo $this->logEntry("Composer downloaded successfully. Making composer.phar executable..."); + // CD into DOCUMENT_ROOT parent and make composer.phar executable + exec("cd " . dirname($this->documentRoot) . " && chmod +x composer.phar"); + } else { + echo $this->logEntry("Could not get Composer working. Please check your settings and try again."); + } + } else { + // Check that composer is working + $check_composer = shell_exec(dirname($this->documentRoot) . "/composer.phar" . ' --version 2>&1'); + echo $this->logEntry($check_composer); + if (strpos($check_composer, 'omposer version')) { + // Check for Composer updates + $update_composer = shell_exec(dirname($this->documentRoot) . "/composer.phar self-update 2>&1"); + echo $this->logEntry("Checking For Composer Update..."); + echo $this->logEntry($update_composer); + } + } + } + + private function checkGit() + { + // Check that git is installed + $check_git = shell_exec($this->gitBinPath . " --version"); + echo $this->logEntry($check_git); + if (!strpos($check_git, 'it version ')) { + die($this->logEntry("
Git is required in order for auto deployment to work. ".
+                "Please check the deploy.init.log for errors.
")); + } + } + + private function checkInstallRepo() + { + + // Create repository directory if it doesnt exist. + if (!is_dir($this->repoDir)) { + echo $this->logEntry("Repository directory does not exist. Creating it now..."); + if (!mkdir($this->repoDir)) { + die($this->logEntry("There was an error creating the repository directory. ". + "Please check your PHP user's permissions and try again.")); + } else { + echo $this->logEntry("Repository directory created successfully."); + + // Initialize and prepare the git repository + $this->initializeRepository(); + + // Update composer + $this->updateComposer(); + } + } else { + if (!is_file($this->repoDir . '/config')) { + echo $this->logEntry("Repository doesn't exist. Attempting to create now..."); + // Initialize and prepare the git repository + $this->initializeRepository(); + + // Update the repository + $this->updateRepository(); + + // Update composer + $this->updateComposer(); + } else { + // Update the repository. + $this->updateRepository(); + + // Update composer + $this->updateComposer(); + } + } + } + + private function initializeRepository() + { + + // Check Known Hosts file for github.com and add using ssh-keyscan + if (is_file('~/.ssh/known_hosts')) { + $known_hosts = file_get_contents('~/.ssh/known_hosts'); + if (!strpos($known_hosts, "github.com")) { + // Add Github to the known hosts + $add_github = shell_exec('ssh-keyscan github.com >> ~/.ssh/known_hosts'); + $this->logEntry($add_github); + } + } else { + // Add Github to the known hosts + $add_github = shell_exec('ssh-keyscan github.com >> ~/.ssh/known_hosts'); + } + + // Create the Mirror Repository + $create_repo_mirror = shell_exec( + 'cd ' . $this->repoDir . ' && ' . 'GIT_SSH_COMMAND="ssh -i ' . $this->certPath . ' -F /dev/null" ' . $this->gitBinPath . ' clone --mirror ' . $this->repoUrl . " . 2>&1" + ); + echo $this->logEntry($create_repo_mirror); + if (strpos($create_repo_mirror, 'ermission denied (publickey)') || + strpos($create_repo_mirror, 'Repository not found.') || + strpos($create_repo_mirror, 'and the repository exists')) { + echo $this->logEntry("Access denied to repository... Creating deployment key now..."); + die($this->logEntry( + "Please add the following public key between the dashes to your ". + "deployment keys for the repository and run this script again.\n" . + str_repeat("-", 80) . "\n" . + $this->getDeployKey() . "\n" . + str_repeat("-", 80) + )); + } + + // Do the initial checkout + $git_checkout = shell_exec( + 'cd ' . $this->repoDir . + ' && GIT_WORK_TREE=' . dirname($this->documentRoot) . ' ' . + $this->gitBinPath . ' checkout ' . $this->repoBranch . ' -f 2>&1' + ); + echo $this->logEntry($git_checkout); + // Get the deployment commit hash + $commit_hash = exec('cd ' . $this->repoDir . ' && ' . $this->gitBinPath . ' rev-parse --short HEAD 2>&1'); + echo $this->logEntry("Deployed Commit: " . $commit_hash); + } + + private function updateRepository() + { + + // Fetch any new changes + $git_fetch = exec('cd ' . $this->repoDir . ' && ' . 'GIT_SSH_COMMAND="ssh -i ' . $this->certPath . ' -F /dev/null" ' . $this->gitBinPath . ' fetch 2>&1'); + if (empty($git_fetch)) { + echo $this->logEntry("There is nothing new to fetch from this repository."); + } else { + echo $this->logEntry($git_fetch); + // Do the checkout + shell_exec( + 'cd ' . $this->repoDir . ' && GIT_WORK_TREE=' . dirname($this->documentRoot) . ' ' . + $this->gitBinPath . ' checkout ' . $this->repoBranch . ' -f 2>&1' + ); + // Get the deployment commit hash + $commit_hash = exec( + 'cd ' . $this->repoDir . ' && ' . $this->gitBinPath . ' rev-parse --short HEAD 2>&1' + ); + echo $this->logEntry("Deployed Commit: " . $commit_hash); + } + } + + private function updateComposer() + { + $checkLockFile = exec('cd ' . $this->repoDir . ' && ' . $this->gitBinPath . ' ls-files --error-unmatch composer.lock 2>&1'); + if (strpos($checkLockFile, 'did not match any file')) { + $update_composer = shell_exec( + 'cd ' . dirname($this->documentRoot) . ' && ' . + dirname($this->documentRoot) . '/composer.phar update --no-dev 2>&1' + ); + } else { + $update_composer = shell_exec( + 'cd ' . dirname($this->documentRoot) . ' && ' . + dirname($this->documentRoot) . '/composer.phar install --no-dev 2>&1' + ); + } + + echo $this->logEntry($update_composer); + if (!strpos($update_composer, 'Generating autoload files')) { + echo $this->logEntry("An error might have occured while updating composer. ". + "Please check the deployment log to confirm."); + } else { + echo $this->logEntry("Composer updated successfully!"); + } + } + + private function getDeployKey() + { + // Create certificate folder if it does not exist + if (!is_dir($this->certFolder)) { + mkdir($this->certFolder, 0755, true); + } + + if (file_exists($this->certFolder . '/' . $this->certFileName) && + file_exists($this->certFolder . '/' . $this->certFileName . ".pub")) { + return file_get_contents($this->certFolder . '/' . $this->certFileName . ".pub"); + } elseif (!file_exists($this->certFolder . '/' . $this->certFileName)) { + // Create the deploy key with ssh-keygen + $generate_key = exec( + "ssh-keygen -q -N '' -t rsa -b 4096 -f " . $this->certFolder . "/" . $this->certFileName + ); + echo $this->logEntry($generate_key); + // Get the contents of the public key + $public_key = file_get_contents($this->certFolder . '/' . $this->certFileName . '.pub'); + // Install the deploy key if not already done + + // Return the public key + return $public_key; + } else { + // Install the deploy key if not already done + + // Return the public key + return file_get_contents($this->certFolder . '/' . $this->certFileName . '.pub'); + } + } + + private function checkConnection() + { + $output == array(); + + $database = $this->settings->db->{$this->settings->environment}; + + $output['check_file'] = false; + $output['check_construct'] = false; + + + // Check the mysql database in the settings file. + if ($database->host != "" + && $database->database != "" + && $database->username != "" + && $database->password != "") { + if (!@mysqli_connect( + $database->host, + $database->username, + $database->password, + $database->database + )) { + echo $this->logEntry("MySQL Connection Error: " . mysqli_connect_error()); + } else { + echo $this->logEntry("Successfully connected to " . $database->database . "."); + $output['check_file'] = true; + } + } + + if ($output['check_file'] == false && $output['check_construct'] == false) { + die($this->logEntry("Could not successfully connect to a database. ". + "Please check your settings and run this script again.")); + } else { + return $output; + } + } + + private function checkPhinx() + { + + // Check if Phinx is installed + if (!is_file(dirname($this->documentRoot) . '/vendor/robmorgan/phinx/bin/phinx')) { + // Install/Update Phinx globally in composer + $install_phinx = shell_exec( + "cd " . dirname($this->documentRoot) . " && ./composer.phar require robmorgan/phinx 2>&1" + ); + echo $this->logEntry($install_phinx); + if (!strpos($install_phinx, 'for robmorgan/phinx')) { + die($this->logEntry("Phinx is required as a global composer dependency. ". + "Please check that it is installed and your web user has access to it.")); + } + } + + // Check that phinx was installed properly + $check_phinx = shell_exec(dirname($this->documentRoot) . "/vendor/robmorgan/phinx/bin/phinx --version"); + echo $this->logEntry($check_phinx); + if (!strpos($check_phinx, 'https://phinx.org')) { + die($this->logEntry("Phinx is required in order for database migration to work. ". + "Please check the deploy.init.log for errors.")); + } + + // Check for Phinx config file + if (!is_file(dirname($this->documentRoot) . '/phinx.php')) { + die($this->logEntry("You do not appear to have a valid phinx.php file in your project root directory.")); + } else { + echo $this->logEntry("Phinx config: phinx.php found."); + } + } + + private function migrateUp() + { + $migrate_up = shell_exec( + "cd " . dirname($this->documentRoot) . " && ./vendor/robmorgan/phinx/bin/phinx migrate 2>&1" + ); + echo $this->logEntry($migrate_up); + if (!strpos($migrate_up, 'All Done.')) { + echo $this->logEntry("There might have been an error in the database migration. ". + "Please check the logs to be sure."); + } else { + echo $this->logEntry("Database migration completed successfully."); + } + } + + private function logEntry($log_text, $return = true) + { + + // Create log folder if it does not exist + if (!is_dir(dirname($this->logFile))) { + mkdir(dirname($this->logFile), 0755, true); + } + + // Create Log File if it does not exist + if (!file_exists($this->logFile)) { + touch($this->logFile); + } + + // Add Log Entry to file + file_put_contents($this->logFile, date('m/d/Y h:i:s a') . " " . $log_text . "\n", FILE_APPEND); + + if ($return == true) { + return "
" . date('m/d/Y h:i:s a') . " $log_text" . "
"; + } + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f49f912 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "dappur/dappurware-depoloyment", + "description": "Deployment support package for the Dappur Framework", + "keywords": ["dappur", "dappurware", "framework", "deploy"], + "version": "4.0.0", + "homepage": "https://github.com/dappur/dappurware", + "license": "MIT", + "authors": [ + { + "name": "Edward Teach", + "email": "edward@edwardteach.co", + "homepage": "https://github.com/dappur" + } + ], + "autoload": { + "psr-4": { + "Dappur\\": "app/src" + } + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.2", + "phpmd/phpmd": "^2.6" + } +}