diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d77009a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cmake-* +build/ +build-artifact/ diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..d860259 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +* Wed Mar 4 2020 Matiss Treinis - 1.0.0 +- Initial public release diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..efc5901 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.11.4) + +# IMPORTANT: updating version might require update in package dependencies at the end of this file. +set(KAFE_VERSION "1.0.0") +set(KAFE_VERSION_INT 10) +set(KAFE_VERSION_DEP_NEXT_MAJOR "2.0.0") + +project(kafe_all VERSION ${KAFE_VERSION} LANGUAGES CXX C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(FIND_LIBRARY_USE_LIB64_PATHS ON) + +#if (NOT UNIX AND NOT APPLE) +if (NOT UNIX) +# message(FATAL_ERROR "Not Unix or Apple, your operating system is not supported!") + message(FATAL_ERROR "Not Unix - your operating system is not supported!") +endif () + +if (CMAKE_BUILD_TYPE MATCHES Debug) + message(STATUS "CMAKE IN DEBUG MODE") +elseif (CMAKE_BUILD_TYPE MATCHES Release) + message(STATUS "CMAKE IN RELEASE MODE") +endif () + +add_subdirectory(libkafe) +add_subdirectory(cli) + +set(CPACK_PACKAGE_NAME "kafe") +set(CPACK_PACKAGE_VENDOR "Matiss Treinis") +set(CPACK_PACKAGE_CONTACT "mrtreinis@gmail.com") +set(CPACK_PACKAGE_DESCRIPTION "Kafe is an open source scriptable systems automation toolkit.") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Kafe is an open source scriptable systems automation toolkit.") +set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/libkafe/kafe") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/COPYRIGHT") + +set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP) + +set(CPACK_COMPONENT_CLI_DESCRIPTION "Kafe command-line interface.") +set(CPACK_COMPONENT_LIBKAFE_DESCRIPTION "Kafe shared library (libkafe)") +set(CPACK_COMPONENT_LIBKAFE-DEV_DESCRIPTION "Kafe C++ development headers (libkafe)") + +set(CPACK_COMPONENT_CLI_DEPENDS libkafe) +set(CPACK_COMPONENT_LIBKAFE-DEV_DEPENDS libkafe) + +# TODO - darwin + +# RPM +set(CPACK_RPM_FILE_NAME RPM-DEFAULT) +set(CPACK_RPM_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_RPM_COMPONENT_INSTALL ON) +set(CPACK_RPM_PACKAGE_LICENSE "Apache 2.0") +set(CPACK_RPM_CLI_PACKAGE_NAME "kafe-cli") +set(CPACK_RPM_LIBKAFE_PACKAGE_NAME "libkafe") +set(CPACK_RPM_LIBKAFE-DEV_PACKAGE_NAME "libkafe-devel") +set(CPACK_RPM_PACKAGE_AUTOREQ OFF) +set(CPACK_RPM_INSTALL_WITH_EXEC ON) +set(CPACK_RPM_LIBKAFE_PACKAGE_AUTOPROV ON) +set(CPACK_RPM_CHANGELOG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/CHANGELOG") +set(CPACK_RPM_CLI_PACKAGE_REQUIRES "libkafe >= ${KAFE_VERSION}, libkafe < ${KAFE_VERSION_DEP_NEXT_MAJOR}, libstdc++ >= 4.8.1, glibc >= 2.0, libgcc >= 4.8.1") +set(CPACK_RPM_LIBKAFE_PACKAGE_REQUIRES "libstdc++ >= 4.8.1, glibc >= 2.0, libgcc >= 4.8.1, lua >= 5.3, libssh >= 0.7.1, libarchive >= 3, libcurl >= 7, libgit2 >= 0.24") +set(CPACK_RPM_LIBKAFE-DEV_PACKAGE_REQUIRES "libkafe = ${KAFE_VERSION}") + +# DEB +set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) +set(CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS OFF) +set(CPACK_DEB_COMPONENT_INSTALL ON) +set(CPACK_DEBIAN_CLI_PACKAGE_NAME "kafe-cli") +set(CPACK_DEBIAN_LIBKAFE_PACKAGE_NAME "libkafe") +set(CPACK_DEBIAN_LIBKAFE-DEV_PACKAGE_NAME "libkafe-dev") +set(CPACK_DEBIAN_CLI_PACKAGE_DEPENDS "libkafe (>=${KAFE_VERSION}), libkafe (<<${KAFE_VERSION_DEP_NEXT_MAJOR}), libstdc++6, libc6, libgcc1, libc6") +set(CPACK_DEBIAN_LIBKAFE_PACKAGE_DEPENDS "libstdc++6, libc6, libgcc1, libc6, liblua5.3-0, libssh-4 (>=0.7.0), libarchive13, libcurl3 | libcurl4, libgit2-24 | libgit2-26 | libgit2-27 | libgit2-28") +set(CPACK_DEBIAN_LIBKAFE-DEV_PACKAGE_DEPENDS "libkafe (=${KAFE_VERSION})") + +set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) +include(CPack) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f362c87 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing to Kafe + +You are welcome to contribute in any way you see fit - code contributions, bug fixes, documentation, issues are all welcome. + +When submitting issues, please include as much information as possible - as a minimum, debug log output, details about +your local and remote system, kafe and libkafe versions. + +By submitting code to this project, you confirm that all your source contributions shall be licensed under +Apache 2.0 license. + +Please, open a ticket or comment on existing ticket before working on any code changes to avoid code conflicts +and to confirm that your changes can be incorporated in the codebase before spending time implementing them. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..53eb17e --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +Kafe (`/ka'fe:/`) is an open source scriptable systems automation toolkit. It provides a basic set of features to interact +with local and remote systems over SSH. Kafe is well suited for application deployment and similar +remote systems administration tasks. + +## Downloads + +See [releases](https://github.com/libkafe/kafe/releases). + +Binary builds are available for: + +- **CentOS** and **RHEL** versions 71, 8 +- **Fedora** versions 31, 32, 33 +- **Ubuntu** versions 18.04, 19.10, 20.04 +- **Debian** versions 9, 10, 11 + +You should be able to use these binary packages for any derivative distributions. +For example, Elementary OS 5.1 users can use Ubuntu 18.04 packages, since Elementary OS is based on Ubuntu 18.04. + +**macOS is officially supported**, but binary builds are not provided. See bellow how to compile +Kafe from source - building Kafe on macOS is fairly trivial. + +1 - EL7 does not ship with Lua 5.3. We currently use +[Cheese](http://www.nosuchhost.net/~cheese/fedora/packages/epel-7/x86_64/cheese-release.html) repository +to obtain Lua 5.3 on EL7. + +### Installation + +Installing a binary build is easy - download the binary packages from [releases](https://github.com/libkafe/kafe/releases) +and install using your operating systems package manager. You will need to download both `kafe-cli` and `libkafe` +artifacts. You do not need to install `libkafe-dev` unless you aim to embed libkafe. + +You can also build the sources locally using `CMake`. See bellow for instructions to build from source. + +## Writing Kafe scripts + +You can declare your server inventory and automation tasks in Kafe scripts. Kafe scripts are written in +[Lua](https://www.lua.org) programming language. Kafe CLI looks for a file named `kafe.lua` +in current working directory to execute the tasks from. + +### Why Lua? + +Lua is widely used, minimal and easy to learn programming language. It is easy to adopt and has +[extensive documentation](http://www.lua.org/docs.html) with large and stable standard library - +a perfect fit for scripting against Kafe APIs. + +### Main concepts + +#### Tasks + +All automation code in Kafe is divided in work units called tasks. A task is isolated unit of work that describes +all actions needed to perform against local or remote systems. + +Tasks are independent, and there are no facilities to chain or relate tasks - this is by design. To share code and +operations between tasks, you can use standard Lua functions. + +#### Inventory + +Kafe categorizes all remote servers by environment and role. And environment is a logical unit of remote servers - +testing environment, staging environment, production environment, and so forth. A role is a logical unit +of one or more servers performing the same infrastructure role - a database server, a web server, an application host, to +name a few. Each inventory item can have one or more roles and environment. + +All remote servers in Kafe must be placed in inventory. Inventory is a list of remote servers with associated +roles and environments. Each server can have one or more roles and can be present in one or more environments. +Each inventory line uniquely combines remote server address, one role, and one environment. To add the same +remote server to multiple roles and environments duplicate the inventory definition line. + +#### Execution of tasks + +All tasks are executed in order as invoked by the script (top-down). This is by design. Parallelization of +remote management tasks might seem a good idea at first, but it rarely is - parallelization makes remote error +handling much more difficult, might leave remote systems in an inconsistent state and requires a +much larger investment in writing the automation tasks. + +### Examples + +You can find examples of how to write Kafe scripts in the [examples](./examples) directory. + +### Scripting API + +Scripting API documentation for API level 1 is available [here](docs/SCRIPTING_API_L1.md). + +### Using the CLI + +To execute a Kafe script, you need to invoke `kafe` command with sub-command `do`. You need to pass +the desired execution environment and a list of one or more tasks to be executed, separated by a comma. + +For example, to execute example tasks named `deploy`, `verify` on `production` environment, you would +execute following Kafe CLI command: + +`kafe do production deploy,verify` + +When executed, Kafe CLI will look for a file named `kafe.lua` in the current working direcory. This +file will be interpreted and requested tasks from it will be executed against all relevant remote servers. + +### Debugging + +You can change the logging level of the CLI tool by setting `KAFE_LOG_LEVEL` environment variable. For example: + +`KAFE_LOG_LEVEL=1 kafe do staging deploy` + +Available log levels are: + +ALL = 0, TRACE = 1, DEBUG = 2, INFO = 3, SUCCESS = 4, WARNING = 5, ERROR = 6, NONE = 7. + +## Kafe and libkafe + +Kafe is written entirely in C++ - it is distributed as a native binary (kafe-cli) and a shared library (libkafe) +with C++ development headers made available for embedding (libkafe-dev). + +## Operating system support + +Kafe should work with any UNIX-like operating system, provided all external dependencies are met. +Binary packages are provided for: + +- **CentOS** and **RHEL** versions 7, 8 +- **Fedora** versions 31, 32, 33 +- **Ubuntu** versions 18.04, 19.10, 20.04 +- **Debian** versions 9, 10, 11 +- **macOS** versions 10.15 Catalina + +Windows is not supported and there are no plans to support Windows builds. Windows users +should be able to use Windows Subsystem for Linux to run or build Kafe. Take note that contributions +and issues related to support for Windows will not be accepted. + +macOS is officially supported for versions 10.15 Catalina and newer. It is entirely possible Kafe works just +fine on older versions of macOS, but no testing has been done on them. If you are unable to upgrade +to latest macOS version, you should still try to install Kafe from sources. It will most likely work +just fine provided all external dependencies are met. + +If you would like to contribute your distribution to the above list, you can implement a build for it. +Just follow the samples already present in [dist](./dist) folder. Any contributions adding support for +new distributions must contain all current non-EOL distribution versions, including RC versions, if any. + +#### Does Kafe have APT / YUM repositories? + +Not at the moment. Having repositories for CentOS, RHEL, Fedora, Debian and Ubuntu is something that I plan +to do in the future. + +#### Is Kafe available on Homebrew? + +No, but it is planned. + +#### Does Kafe have official Docker images with binaries? + +No and it is unlikely there ever will be. At this time, Kafe is primarily a CLI tool - having a Docker image for it +has as much benefit as having a Docker image for `tail`. If you need to have Kafe in Docker for CI or some other use, +creating your own image using any of the supported base operating system images should be trivial. + +## Backwards compatibility + +### Kafe CLI and libkafe + +Kafe CLI should be compatible with libkafe within the same major version. + +### libkafe + +Libkafe development libraries are guaranteed to be compatible within the same major version. + +### Scripting API + +Scripting API is versioned separately from CLI and libkafe. Scripting API compatibility is guaranteed +within the same API version, meaning, any script built for API version 1 will work with any Kafe version supporting +version 1 regardless of the internals of Kafe itself. + +| libkafe version | API level | Lua | Status | +|-----------------|--------------|----------|------------| +| 1.x | 1 | 5.3, 5.4 | Locked | +| 2.x | 21| 5.3, 5.4 | Planned | + +1 - it is very likely API level 2 will be fully backward compatible with level 1. + +### Lua compatibility + +Kafe currently supports Lua version 5.3 with 5.4 support in development. Lua versions +older than 5.3 are not supported. Kafe will build against the latest available version of Lua in the +build system. For most part, it is the latest version distributed by the operating system itself with the sole exception +of CentOS 7, where third party repository is required to install and use Kafe. + +### Building from source + +To build Kafe from the source, you will need C++ and C compiler, CMake (version 3.11.4 or newer), Make and following +development libraries: + +- liblua version 5.3 or newer (up to version 5.4) +- libcurl (reserved for future APIs) +- libarchive +- libssh +- libgit2 (reserved for future APIs) + +Kafe is built and tested using Clang toolchain - version 7 or higher, depending on build environment, +both in development and distribution. You should be able to build your own binaries using any +C11 and C++17 compatible C/C++ compiler toolchain. + +See [/dist/](./dist) directory for example Docker build files and scripts per each supported build target. + +You can also refer to [build-dist.sh](./build-dist.sh) for how to build distribution specific packages +using Docker. + +#### Building on macOS + +To build from sources on macOS you will need `git` and Kafe dependencies installed on your system. + +To install dependencies using Homebrew, use this command: + +`brew install llvm lua libssh libgit2 curl libarchive` + +To build +1. Clone the source from `git@github.com:libkafe/kafe.git`; +2. Optionally, check out the desired version tag to build from GIT; +3. Execute [build-dist-macos.sh](./build-dist-macos.sh) file found at the root of the cloned sources. +4. Optionally, enter `build/osx` directory, and issue `make install` command to install Kafe on your local machine. + +## Future expansions + +Scripting API level 1 ships with a basic set of commands required for remote deployment and maintenance. It is considered +feature complete and no new features will be added to this API level. + +Scripting API level 2 will incorporate more features to simplify application deployment and systems operations. Following +is planned for level 2 expansion: + +- `.kafeignore` support for archive and directory operations. +- Local and remote file system utility functions, expanded standard operations library. +- Subset of Git features to streamline working with repositories (cloning, pulling, pushing, tagging, reading tags, releases...). +- Simplistic CURL based HTTP client (basic requests, file uploads and downloads). +- Test framework for Kafe scripts. + +A side-goal of API level 2 is to retain compatibility with API level 1, meaning, if at all possible, all scripts written +for API level 1 will work just fine with API level 2 compatible libkafe. + +## History + +Kafe is nearly complete rewrite of OPM (Optional Package Manager) - a command-line tool I wrote several years ago for my +personal use to automate application and content deployment. It was written entirely in C using Python as a scripting +language to interface against remote APIs. OPMs design had several drawbacks, namely very tight integration with specific +Python version and gratuitous use of static linking against certain 3rd party dependencies. It worked (and still does!) +just fine, but was not suitable at all to be open sourced nor was it easily sharable - meaning it was fairly useless +outside the realm of my own personal use. + +[Hemp](https://github.com/Addvilz/hemp) was another tool I wrote prior to Kafe for use in production to automate deployment +and remote automation. This tool was based on now unmaintained 3rd party remote automation library and was deprecated +due to lack of support for Python 3 and breaking changes from upstream library vendor. Unfortunately, Hemp was and still +is being used in a number of production projects and many production systems rely on it being functional and maintained, +something that is no longer viable. + +I created Kafe to replace both of these tools. Kafe is designed to be as minimal and straightforward as possible. +I wanted to create an automation tool with strong backwards compatibility guarantees, meaning that once written, +automation tasks should work with minimal or no changes years to come. + +## License + +Licensed under the Apache License, Version 2.0 (the "License"). See [LICENSE](./LICENSE) for more details. diff --git a/build-dist-macos.sh b/build-dist-macos.sh new file mode 100755 index 0000000..d575c65 --- /dev/null +++ b/build-dist-macos.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -xe + +# Build OSX from current directory. + +mkdir -p build/osx/ +cd build/osx + +export CC=/usr/local/opt/llvm/bin/clang +export CXX=/usr/local/opt/llvm/bin/clang++ + +cmake ../.. +make \ No newline at end of file diff --git a/build-dist.sh b/build-dist.sh new file mode 100755 index 0000000..6c69c5f --- /dev/null +++ b/build-dist.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -xe + +# TODO - I do not like this copy paste... + +rm -rf build/ +rm -rf build-artifact/ + +# CentOS 7 +if [[ "$(docker images -q "kafe/centos:7-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/centos:7-build dist/centos/7 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/centos:7-build +mkdir -p build-artifact/centos-7/ +cp build/centos/7/kafe-cli-*.rpm build-artifact/centos-7/ +cp build/centos/7/libkafe-*.rpm build-artifact/centos-7/ +for f in build-artifact/centos-7/*; do mv -v "$f" $(echo "$f" | sed "s/\.rpm/\.el7\.rpm/"); done + +# CentOS 8 +if [[ "$(docker images -q "kafe/centos:8-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/centos:8-build dist/centos/8 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/centos:8-build +mkdir -p build-artifact/centos-8/ +cp build/centos/8/kafe-cli-*.rpm build-artifact/centos-8/ +cp build/centos/8/libkafe-*.rpm build-artifact/centos-8/ +for f in build-artifact/centos-8/*; do mv -v "$f" $(echo "$f" | sed "s/\.rpm/\.el8\.rpm/"); done + +# Debian 9 +if [[ "$(docker images -q "kafe/debian:9-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/debian:9-build dist/debian/9 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/debian:9-build +mkdir -p build-artifact/debian-9/ +cp build/debian/9/kafe-cli*.deb build-artifact/debian-9/ +cp build/debian/9/libkafe*.deb build-artifact/debian-9/ +for f in build-artifact/debian-9/*; do mv -v "$f" $(echo "$f" | sed "s/\.deb/\.deb9\.deb/"); done + +# Debian 10 +if [[ "$(docker images -q "kafe/debian:10-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/debian:10-build dist/debian/10 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/debian:10-build +mkdir -p build-artifact/debian-10/ +cp build/debian/10/kafe-cli*.deb build-artifact/debian-10/ +cp build/debian/10/libkafe*.deb build-artifact/debian-10/ +for f in build-artifact/debian-10/*; do mv -v "$f" $(echo "$f" | sed "s/\.deb/\.deb10\.deb/"); done + +# Debian 11 +if [[ "$(docker images -q "kafe/debian:11-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/debian:11-build dist/debian/11 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/debian:11-build +mkdir -p build-artifact/debian-11/ +cp build/debian/11/kafe-cli*.deb build-artifact/debian-11/ +cp build/debian/11/libkafe*.deb build-artifact/debian-11/ +for f in build-artifact/debian-11/*; do mv -v "$f" $(echo "$f" | sed "s/\.deb/\.deb11\.deb/"); done + +# Ubuntu 18.04 +if [[ "$(docker images -q "kafe/ubuntu:1804-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/ubuntu:1804-build dist/ubuntu/1804 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/ubuntu:1804-build +mkdir -p build-artifact/ubuntu-1804/ +cp build/ubuntu/1804/kafe-cli*.deb build-artifact/ubuntu-1804/ +cp build/ubuntu/1804/libkafe*.deb build-artifact/ubuntu-1804/ +for f in build-artifact/ubuntu-1804/*; do mv -v "$f" $(echo "$f" | sed "s/\.deb/\.ubu1804\.deb/"); done + +# Ubuntu 19.10 +if [[ "$(docker images -q "kafe/ubuntu:1910-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/ubuntu:1910-build dist/ubuntu/1910 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/ubuntu:1910-build +mkdir -p build-artifact/ubuntu-1910/ +cp build/ubuntu/1910/kafe-cli*.deb build-artifact/ubuntu-1910/ +cp build/ubuntu/1910/libkafe*.deb build-artifact/ubuntu-1910/ +for f in build-artifact/ubuntu-1910/*; do mv -v "$f" $(echo "$f" | sed "s/\.deb/\.ubu1910\.deb/"); done + +# Ubuntu 20.04 +if [[ "$(docker images -q "kafe/ubuntu:2004-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/ubuntu:2004-build dist/ubuntu/2004 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/ubuntu:2004-build +mkdir -p build-artifact/ubuntu-2004/ +cp build/ubuntu/2004/kafe-cli*.deb build-artifact/ubuntu-2004/ +cp build/ubuntu/2004/libkafe*.deb build-artifact/ubuntu-2004/ +for f in build-artifact/ubuntu-2004/*; do mv -v "$f" $(echo "$f" | sed "s/\.deb/\.ubu2004\.deb/"); done + +# Fedora 31 +if [[ "$(docker images -q "kafe/fedora:31-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/fedora:31-build dist/fedora/31 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/fedora:31-build +mkdir -p build-artifact/fedora-31/ +cp build/fedora/31/kafe-cli-*.rpm build-artifact/fedora-31/ +cp build/fedora/31/libkafe-*.rpm build-artifact/fedora-31/ +for f in build-artifact/fedora-31/*; do mv -v "$f" $(echo "$f" | sed "s/\.rpm/\.f31\.rpm/"); done + +# Fedora 32 +if [[ "$(docker images -q "kafe/fedora:32-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/fedora:32-build dist/fedora/32 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/fedora:32-build +mkdir -p build-artifact/fedora-32/ +cp build/fedora/32/kafe-cli-*.rpm build-artifact/fedora-32/ +cp build/fedora/32/libkafe-*.rpm build-artifact/fedora-32/ +for f in build-artifact/fedora-32/*; do mv -v "$f" $(echo "$f" | sed "s/\.rpm/\.f32\.rpm/"); done + +# Fedora 33 +if [[ "$(docker images -q "kafe/fedora:33-build" 2> /dev/null)" == "" ]]; then + docker build -t kafe/fedora:33-build dist/fedora/33 +fi + +docker run -it --rm -v `pwd`:/kafe kafe/fedora:33-build +mkdir -p build-artifact/fedora-33/ +cp build/fedora/33/kafe-cli-*.rpm build-artifact/fedora-33/ +cp build/fedora/33/libkafe-*.rpm build-artifact/fedora-33/ +for f in build-artifact/fedora-33/*; do mv -v "$f" $(echo "$f" | sed "s/\.rpm/\.f33\.rpm/"); done \ No newline at end of file diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt new file mode 100644 index 0000000..403681f --- /dev/null +++ b/cli/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.11.4) +project(kafe_cli VERSION ${KAFE_VERSION} LANGUAGES CXX C) + +message(STATUS "kafe cli version is defined as ${PROJECT_VERSION}") + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(FIND_LIBRARY_USE_LIB64_PATHS ON) + +if(NOT UNIX AND NOT APPLE) + message( FATAL_ERROR "Not Unix or Apple, your operating system is not supported!" ) +endif() + +if(APPLE) + set(CMAKE_PREFIX_PATH "/usr/local/opt/libarchive/;/usr/local/opt/libssh/;/usr/local/opt/curl/;/usr/local/opt/libgit2/;/usr/local/opt/lua/") +endif() + +if (CMAKE_BUILD_TYPE MATCHES Debug) + message(STATUS "CMAKE IN DEBUG MODE") +elseif(CMAKE_BUILD_TYPE MATCHES Release) + message(STATUS "CMAKE IN RELEASE MODE") +endif () + +find_package(Lua 5.3 REQUIRED) +find_package(LIBSSH 0.7 REQUIRED) +find_package(LibArchive 3.1.2 REQUIRED) +find_package(CURL 7.11 REQUIRED) +find_package(LIBGIT2 REQUIRED) +find_package(Filesystem COMPONENTS Experimental Final REQUIRED) + +add_executable(kafe_cli main.cpp main.hpp logger.hpp logger.cpp) + +target_link_libraries(kafe_cli PRIVATE kafe_lib_shared) + +target_include_directories(kafe_cli PRIVATE ${LUA_INCLUDE_DIR}) +target_include_directories(kafe_cli PRIVATE ${LIBSSH_INCLUDE_DIR}) +target_include_directories(kafe_cli PRIVATE ${LibArchive_INCLUDE_DIRS}) +target_include_directories(kafe_cli PRIVATE ${CURL_INCLUDE_DIRS}) +target_include_directories(kafe_cli PRIVATE ${LIBGIT2_INCLUDE_DIR}) + +string(TIMESTAMP KAFE_CMAKE_LIB_BUILD_TS "%Y-%m-%d" UTC) +target_compile_definitions(kafe_cli PUBLIC KAFE_CMAKE_CLI_BUILD_TS="${KAFE_CMAKE_LIB_BUILD_TS}") + +target_compile_definitions(kafe_cli PUBLIC KAFE_CMAKE_CLI_VERSION="${PROJECT_VERSION}") +target_compile_definitions(kafe_cli PUBLIC KAFE_CMAKE_CLI_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}) +target_compile_definitions(kafe_cli PUBLIC KAFE_CMAKE_CLI_VERSION_MINOR=${PROJECT_VERSION_MINOR}) +target_compile_definitions(kafe_cli PUBLIC KAFE_CMAKE_CLI_VERSION_RELEASE=${PROJECT_VERSION_PATCH}) + +find_path(KAFE_CMAKE_LIB_LUA_54 lua5.4/lua.h PATHS ${LUA_INCLUDE_DIR}) +find_path(KAFE_CMAKE_LIB_LUA_53 lua5.3/lua.h PATHS ${LUA_INCLUDE_DIR}) +find_path(KAFE_CMAKE_LIB_LUA_ROOT lua.h PATHS ${LUA_INCLUDE_DIR}) + +if (KAFE_CMAKE_LIB_LUA_54) + target_include_directories(kafe_cli PRIVATE ${KAFE_CMAKE_LIB_LUA_54}) +elseif (KAFE_CMAKE_LIB_LUA_53) + target_include_directories(kafe_cli PRIVATE ${KAFE_CMAKE_LIB_LUA_53}) +elseif (KAFE_CMAKE_LIB_LUA_ROOT) + target_include_directories(kafe_cli PRIVATE ${KAFE_CMAKE_LIB_LUA_ROOT}) +else () + message(FATAL_ERROR "Could not determine Lua header to include. Expected either or ") +endif () + +set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "kafe") +include(GNUInstallDirs) +install(TARGETS kafe_cli DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT cli) diff --git a/cli/logger.cpp b/cli/logger.cpp new file mode 100644 index 0000000..bec0dbe --- /dev/null +++ b/cli/logger.cpp @@ -0,0 +1,241 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include "logger.hpp" + +using namespace std; +using namespace kafe; + +namespace kafe { + static LogLevel get_env_log_level() { + char *level = getenv("KAFE_LOG_LEVEL"); + + if (nullptr == level) { + return LogLevel::INFO; + } + + int i_level; + try { + i_level = stoi(level); + } catch (exception &e) { + std::cerr << "Logger error: unable to set log level from environment, expected integer level, got <" + << level + << ">" + << endl; + return LogLevel::INFO; + } + + if (LogLevel::ALL > i_level || LogLevel::NONE < i_level) { + std::cerr << "Logger error: invalid log level " << level << ", ignoring" << endl; + } else { + return static_cast(i_level); + } + + return LogLevel::INFO; + } + + static string level_to_string(LogLevel level) { + switch (level) { + case ALL: + return " ALL"; + case NONE: + return " NONE"; + case TRACE: + return " TRACE"; + case DEBUG: + return " DEBUG"; + case INFO: + return " INFO"; + case SUCCESS: + return "SUCCESS"; + case WARNING: + return "WARNING"; + case ERROR: + return " ERROR"; + } + } + + static string context_to_s(const vector &context) { + if (context.empty()) { + return ""; + } + + ostringstream ss; + + ss << IO_TTY_ANSI_COLOR_MAGENTA; + + for (auto &ctx : context) { + ss << '[' << ctx << "] "; + } + + ss << IO_TTY_ANSI_COLOR_RESET; + + return ss.str(); + } + + inline string expand_prefix(const string& level) { + char buffer[sizeof(char) * 18]; + sprintf(buffer, "%*s", 18, level.c_str()); + return string(buffer); + } + + void Logger::on_log(const LogEvent &event) const { + string color; + + switch (event.get_level()) { + case NONE: + case ALL: + case TRACE: + case DEBUG: + color = ""; + break; + case INFO: + color = IO_TTY_ANSI_COLOR_BLUE; + break; + case SUCCESS: + color = IO_TTY_ANSI_COLOR_GREEN; + break; + case WARNING: + color = IO_TTY_ANSI_COLOR_YELLOW; + break; + case ERROR: + color = IO_TTY_ANSI_COLOR_RED; + break; + } + + flockfile(stdout); + flockfile(stderr); + + ostringstream time_stream; + + auto etime = chrono::system_clock::to_time_t(event.get_event_time()); + auto tm = *std::localtime(&etime); + time_stream << put_time(&tm, "[%T]"); + const auto *timer = event.get_timer(); + + auto context_s = context_to_s(get_context()); + + if (nullptr == timer) { + fprintf(stdout, + "%s %s%s%s %s%s\n", + time_stream.str().c_str(), + color.c_str(), + level_to_string(event.get_level()).c_str(), + IO_TTY_ANSI_COLOR_RESET, + context_s.c_str(), + event.get_message().c_str() + ); + } else { + fprintf(stdout, + "%s %s%s%s %s%s %s[%s]%s\n", + time_stream.str().c_str(), + color.c_str(), + level_to_string(event.get_level()).c_str(), + IO_TTY_ANSI_COLOR_RESET, + context_s.c_str(), + event.get_message().c_str(), + IO_TTY_ANSI_COLOR_MAGENTA, + timer->duration().c_str(), + IO_TTY_ANSI_COLOR_RESET + ); + } + + funlockfile(stdout); + funlockfile(stderr); + } + + void Logger::on_stdout_line(string line) const { + on_stdout_line("out", line); + } + + void Logger::on_stdout_line(string prefix, string line) const { + size_t len = line.length(); + + if (0 == len) { + return; + } + + flockfile(stdout); + flockfile(stderr); + fprintf( + stdout, + "%s%s%s %s%s", + IO_TTY_ANSI_COLOR_GREEN, + expand_prefix(prefix).c_str(), + IO_TTY_ANSI_COLOR_RESET, + context_to_s(get_context()).c_str(), + line.c_str() + ); + + if (len > 1 && line[len - 1] != '\n') { + fputs("\n", stdout); + } + + funlockfile(stdout); + funlockfile(stderr); + } + + void Logger::on_stderr_line(string line) const { + on_stderr_line("err", line); + } + + void Logger::on_stderr_line(string prefix, string line) const { + size_t len = line.length(); + + if (0 == len) { + return; + } + + flockfile(stdout); + flockfile(stderr); + fprintf( + stderr, + "%s%s%s %s%s", + IO_TTY_ANSI_COLOR_RED, + expand_prefix(prefix).c_str(), + IO_TTY_ANSI_COLOR_RESET, + context_to_s(get_context()).c_str(), + line.c_str() + ); + + if (len > 1 && line[len - 1] != '\n') { + fputs("\n", stderr); + } + + funlockfile(stdout); + funlockfile(stderr); + } + + FILE *Logger::get_stdout() const { + return stdout; + } + + FILE *Logger::get_stderr() const { + return stderr; + } + + LogLevel Logger::get_level() const { + return get_env_log_level(); + } +} \ No newline at end of file diff --git a/cli/logger.hpp b/cli/logger.hpp new file mode 100644 index 0000000..8fb6440 --- /dev/null +++ b/cli/logger.hpp @@ -0,0 +1,48 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef KAFE_CLI_LOGGER_HPP +#define KAFE_CLI_LOGGER_HPP + +#include + +namespace kafe { + class Logger : public ILogEventListener { + public: + [[nodiscard]] LogLevel get_level() const override; + + private: + void on_log(const LogEvent &event) const override; + + void on_stdout_line(string line) const override; + + void on_stdout_line(string prefix, string line) const override; + + void on_stderr_line(string line) const override; + + void on_stderr_line(string prefix, string line) const override; + + [[nodiscard]] FILE *get_stdout() const override; + + [[nodiscard]] FILE *get_stderr() const override; + }; +} + + +#endif diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 0000000..b183690 --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,186 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "main.hpp" +#include "logger.hpp" + +using namespace std; +using namespace kafe; + +#if __APPLE__ +extern char** environ; +#endif + +void print_usage() { + fflush(stderr); + fflush(stdout); + + fprintf(stdout, "kafe, version %s (with libkafe %s, API level %d)\n\n", KAFE_CLI_VERSION, LIBKAFE_VERSION, + LIBKAFE_API_LEVEL); + + cout << "Usage: kafe [arguments, ...]" << endl; + cout << " kafe do " << endl; + cout << " kafe " << endl; + cout << " kafe [--lib]" << endl; + cout << " kafe " << endl; + + cout << "\n"; + + cout << " kafe do: execute tasks from project file with given environment." << endl; + cout << " kafe help: display this help." << endl; + cout << " kafe version: display KAFE program version and exit. Optionally," + " show libkafe version used if argument --lib is set." << endl; + cout << " kafe about: display software credits and licensing information." << endl; + + fflush(stdout); +} + +void print_about() { + fflush(stderr); + fflush(stdout); + + fprintf(stdout, "kafe, version %s, built %s\n\n", KAFE_CLI_VERSION, KAFE_CLI_BUILD_TS); + cout << "Copyright 2020 Matiss Treinis" << endl; + cout << "Licensed under the Apache License, Version 2.0" << endl; + cout << "Homepage : https://github.com/libkafe/kafe" << endl; + cout << "Bug reports: https://github.com/libkafe/kafe/issues" << endl; + + cout << "\n"; + cout << "API level: " << LIBKAFE_API_LEVEL << endl; + cout << "\n"; + + cout << "Using libkafe " << LIBKAFE_VERSION << " (built " << LIBKAFE_BUILD_TS << ")" << endl; + cout << "Using " << LIBKAFE_VERSION_LIB_LUA << endl; + cout << "Using libssh " << LIBKAFE_VERSION_LIB_SSH << endl; + cout << "Using libarchive " << LIBKAFE_VERSION_LIB_ARCHIVE << endl; + cout << "Using CURL " << LIBKAFE_VERSION_LIB_CURL << endl; + cout << "Using libgit2 " << LIBKAFE_VERSION_LIB_GIT2 << endl; + + fflush(stdout); +} + +void print_version() { + fflush(stderr); + fflush(stdout); + cout << KAFE_CLI_VERSION << endl; + fflush(stdout); +} + +void print_lib_version() { + fflush(stderr); + fflush(stdout); + cout << LIBKAFE_VERSION << endl; + fflush(stdout); +} + +vector split_csv_arguments(const string &arguments, char delimiter) { + vector result; + stringstream stream(arguments); + string item; + + while (getline(stream, item, delimiter)) { + result.push_back(item); + } + + return result; +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + cerr << "Missing command to execute"; + print_usage(); + return 1; + } + + if (0 == strcmp("do", argv[1])) { + if (4 != argc) { + cerr << "Command expects exactly two arguments - environment name an" + " comma separated task list.\n" + "Example: kafe do staging task1,task2,task3"; + print_usage(); + return 1; + } + + map envvals; + for (char **current = environ; *current; ++current) { + const string envl = string(*current); + const int pos = envl.find_first_of('='); + pair p = pair( + envl.substr(0, pos), + envl.substr(pos + 1) + ); + envvals.insert(p); + } + + + string environment = argv[2]; + string task_list_s = argv[3]; + vector task_list_v = split_csv_arguments(task_list_s, ','); + + try { + auto project = Project("kafe.lua"); + auto logger = Logger(); + auto context = Context(envvals, environment, task_list_v, &logger); + auto inventory = Inventory(); + project.execute(context, inventory); + } catch (RuntimeException &e) { + cerr << e.what(); + return 1; + } + return 0; + } + + if (0 == strcmp("about", argv[1]) || 0 == strcmp("--about", argv[1])) { + print_about(); + return 0; + } + + if (0 == strcmp("help", argv[1]) || 0 == strcmp("--help", argv[1])) { + print_usage(); + return 0; + } + + if (0 == strcmp("version", argv[1]) || 0 == strcmp("--version", argv[1])) { + if (2 == argc) { + print_version(); + return 0; + } + + if (3 == argc && 0 == strcmp("--lib", argv[2])) { + print_lib_version(); + return 0; + } + + cerr << "Invalid usage of version command"; + return 1; + } + + // Default + cerr << "Unknown command"; + print_usage(); + return 1; +} diff --git a/cli/main.hpp b/cli/main.hpp new file mode 100644 index 0000000..0bf033e --- /dev/null +++ b/cli/main.hpp @@ -0,0 +1,29 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef KAFE_CLI_MAIN_HPP +#define KAFE_CLI_MAIN_HPP + +#define KAFE_CLI_VERSION KAFE_CMAKE_CLI_VERSION +#define KAFE_CLI_BUILD_TS KAFE_CMAKE_CLI_BUILD_TS +#define KAFE_CLI_VERSION_MAJOR KAFE_CMAKE_CLI_VERSION_MAJOR +#define KAFE_CLI_VERSION_MINOR KAFE_CMAKE_CLI_VERSION_MINOR +#define KAFE_CLI_VERSION_RELEASE KAFE_CMAKE_CLI_VERSION_RELEASE + +#endif diff --git a/cmake/Modules/FindFilesystem.cmake b/cmake/Modules/FindFilesystem.cmake new file mode 100644 index 0000000..ddf3045 --- /dev/null +++ b/cmake/Modules/FindFilesystem.cmake @@ -0,0 +1,227 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: + +FindFilesystem +############## + +This module supports the C++17 standard library's filesystem utilities. Use the +:imp-target:`std::filesystem` imported target to + +Options +******* + +The ``COMPONENTS`` argument to this module supports the following values: + +.. find-component:: Experimental + :name: fs.Experimental + + Allows the module to find the "experimental" Filesystem TS version of the + Filesystem library. This is the library that should be used with the + ``std::experimental::filesystem`` namespace. + +.. find-component:: Final + :name: fs.Final + + Finds the final C++17 standard version of the filesystem library. + +If no components are provided, behaves as if the +:find-component:`fs.Final` component was specified. + +If both :find-component:`fs.Experimental` and :find-component:`fs.Final` are +provided, first looks for ``Final``, and falls back to ``Experimental`` in case +of failure. If ``Final`` is found, :imp-target:`std::filesystem` and all +:ref:`variables ` will refer to the ``Final`` version. + + +Imported Targets +**************** + +.. imp-target:: std::filesystem + + The ``std::filesystem`` imported target is defined when any requested + version of the C++ filesystem library has been found, whether it is + *Experimental* or *Final*. + + If no version of the filesystem library is available, this target will not + be defined. + + .. note:: + This target has ``cxx_std_17`` as an ``INTERFACE`` + :ref:`compile language standard feature `. Linking + to this target will automatically enable C++17 if no later standard + version is already required on the linking target. + + +.. _fs.variables: + +Variables +********* + +.. variable:: CXX_FILESYSTEM_IS_EXPERIMENTAL + + Set to ``TRUE`` when the :find-component:`fs.Experimental` version of C++ + filesystem library was found, otherwise ``FALSE``. + +.. variable:: CXX_FILESYSTEM_HAVE_FS + + Set to ``TRUE`` when a filesystem header was found. + +.. variable:: CXX_FILESYSTEM_HEADER + + Set to either ``filesystem`` or ``experimental/filesystem`` depending on + whether :find-component:`fs.Final` or :find-component:`fs.Experimental` was + found. + +.. variable:: CXX_FILESYSTEM_NAMESPACE + + Set to either ``std::filesystem`` or ``std::experimental::filesystem`` + depending on whether :find-component:`fs.Final` or + :find-component:`fs.Experimental` was found. + + +Examples +******** + +Using `find_package(Filesystem)` with no component arguments: + +.. code-block:: cmake + + find_package(Filesystem REQUIRED) + + add_executable(my-program main.cpp) + target_link_libraries(my-program PRIVATE std::filesystem) + + +#]=======================================================================] + + +if(TARGET std::filesystem) + # This module has already been processed. Don't do it again. + return() +endif() + +include(CMakePushCheckState) +include(CheckIncludeFileCXX) +include(CheckCXXSourceCompiles) + +cmake_push_check_state() + +set(CMAKE_REQUIRED_QUIET ${Filesystem_FIND_QUIETLY}) + +# All of our tests required C++17 or later +set(CMAKE_CXX_STANDARD 17) + +# Normalize and check the component list we were given +set(want_components ${Filesystem_FIND_COMPONENTS}) +if(Filesystem_FIND_COMPONENTS STREQUAL "") + set(want_components Final) +endif() + +# Warn on any unrecognized components +set(extra_components ${want_components}) +list(REMOVE_ITEM extra_components Final Experimental) +foreach(component IN LISTS extra_components) + message(WARNING "Extraneous find_package component for Filesystem: ${component}") +endforeach() + +# Detect which of Experimental and Final we should look for +set(find_experimental TRUE) +set(find_final TRUE) +if(NOT "Final" IN_LIST want_components) + set(find_final FALSE) +endif() +if(NOT "Experimental" IN_LIST want_components) + set(find_experimental FALSE) +endif() + +if(find_final) + check_include_file_cxx("filesystem" _CXX_FILESYSTEM_HAVE_HEADER) + mark_as_advanced(_CXX_FILESYSTEM_HAVE_HEADER) + if(_CXX_FILESYSTEM_HAVE_HEADER) + # We found the non-experimental header. Don't bother looking for the + # experimental one. + set(find_experimental FALSE) + endif() +else() + set(_CXX_FILESYSTEM_HAVE_HEADER FALSE) +endif() + +if(find_experimental) + check_include_file_cxx("experimental/filesystem" _CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) + mark_as_advanced(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) +else() + set(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER FALSE) +endif() + +if(_CXX_FILESYSTEM_HAVE_HEADER) + set(_have_fs TRUE) + set(_fs_header filesystem) + set(_fs_namespace std::filesystem) +elseif(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) + set(_have_fs TRUE) + set(_fs_header experimental/filesystem) + set(_fs_namespace std::experimental::filesystem) +else() + set(_have_fs FALSE) +endif() + +set(CXX_FILESYSTEM_HAVE_FS ${_have_fs} CACHE BOOL "TRUE if we have the C++ filesystem headers") +set(CXX_FILESYSTEM_HEADER ${_fs_header} CACHE STRING "The header that should be included to obtain the filesystem APIs") +set(CXX_FILESYSTEM_NAMESPACE ${_fs_namespace} CACHE STRING "The C++ namespace that contains the filesystem APIs") + +set(_found FALSE) + +if(CXX_FILESYSTEM_HAVE_FS) + # We have some filesystem library available. Do link checks + string(CONFIGURE [[ + #include <@CXX_FILESYSTEM_HEADER@> + + int main() { + auto cwd = @CXX_FILESYSTEM_NAMESPACE@::current_path(); + return cwd.string().size(); + } + ]] code @ONLY) + + # Try to compile a simple filesystem program without any linker flags + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED) + + set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED}) + + if(NOT CXX_FILESYSTEM_NO_LINK_NEEDED) + set(prev_libraries ${CMAKE_REQUIRED_LIBRARIES}) + # Add the libstdc++ flag + set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lstdc++fs) + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_STDCPPFS_NEEDED) + set(can_link ${CXX_FILESYSTEM_STDCPPFS_NEEDED}) + if(NOT CXX_FILESYSTEM_STDCPPFS_NEEDED) + # Try the libc++ flag + set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lc++fs) + check_cxx_source_compiles("${code}" CXX_FILESYSTEM_CPPFS_NEEDED) + set(can_link ${CXX_FILESYSTEM_CPPFS_NEEDED}) + endif() + endif() + + if(can_link) + add_library(std::filesystem INTERFACE IMPORTED) + target_compile_features(std::filesystem INTERFACE cxx_std_17) + set(_found TRUE) + + if(CXX_FILESYSTEM_NO_LINK_NEEDED) + # Nothing to add... + elseif(CXX_FILESYSTEM_STDCPPFS_NEEDED) + target_link_libraries(std::filesystem INTERFACE -lstdc++fs) + elseif(CXX_FILESYSTEM_CPPFS_NEEDED) + target_link_libraries(std::filesystem INTERFACE -lc++fs) + endif() + endif() +endif() + +cmake_pop_check_state() + +set(Filesystem_FOUND ${_found} CACHE BOOL "TRUE if we can compile and link a program using std::filesystem" FORCE) + +if(Filesystem_FIND_REQUIRED AND NOT Filesystem_FOUND) + message(FATAL_ERROR "Cannot Compile simple program using std::filesystem") +endif() \ No newline at end of file diff --git a/cmake/Modules/FindLIBGIT2.cmake b/cmake/Modules/FindLIBGIT2.cmake new file mode 100644 index 0000000..4541f39 --- /dev/null +++ b/cmake/Modules/FindLIBGIT2.cmake @@ -0,0 +1,33 @@ +# - Try to find the libgit2 library +# Once done this will define +# +# LIBGIT2_FOUND - System has libgit2 +# LIBGIT2_INCLUDE_DIR - The libgit2 include directory +# LIBGIT2_LIBRARIES - The libraries needed to use libgit2 +# LIBGIT2_DEFINITIONS - Compiler switches required for using libgit2 + + +# use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +#FIND_PACKAGE(PkgConfig) +#PKG_SEARCH_MODULE(PC_LIBGIT2 libgit2) + +SET(LIBGIT2_DEFINITIONS ${PC_LIBGIT2_CFLAGS_OTHER}) + +FIND_PATH(LIBGIT2_INCLUDE_DIR NAMES git2.h + HINTS + ${PC_LIBGIT2_INCLUDEDIR} + ${PC_LIBGIT2_INCLUDE_DIRS} + ) + +FIND_LIBRARY(LIBGIT2_LIBRARIES NAMES git2 + HINTS + ${PC_LIBGIT2_LIBDIR} + ${PC_LIBGIT2_LIBRARY_DIRS} + ) + + +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LIBGIT2 DEFAULT_MSG LIBGIT2_LIBRARIES LIBGIT2_INCLUDE_DIR) + +MARK_AS_ADVANCED(LIBGIT2_INCLUDE_DIR LIBGIT2_LIBRARIES) \ No newline at end of file diff --git a/cmake/Modules/FindLIBSSH.cmake b/cmake/Modules/FindLIBSSH.cmake new file mode 100644 index 0000000..97d1c2d --- /dev/null +++ b/cmake/Modules/FindLIBSSH.cmake @@ -0,0 +1,97 @@ +# - Try to find LibSSH +# Once done this will define +# +# LIBSSH_FOUND - system has LibSSH +# LIBSSH_INCLUDE_DIRS - the LibSSH include directory +# LIBSSH_LIBRARIES - Link these to use LibSSH +# +# Copyright (c) 2009 Andreas Schneider +# Modified by Peter Wu to use standard +# find_package(LIBSSH ...) without external module. +# +# Redistribution and use is allowed according to the terms of the New +# BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# + +if (LIBSSH_LIBRARIES AND LIBSSH_INCLUDE_DIRS) + # in cache already + set(LIBSSH_FOUND TRUE) +else () + find_path(LIBSSH_INCLUDE_DIR + NAMES + libssh/libssh.h + HINTS + "${LIBSSH_HINTS}/include" + PATHS + /usr/include + /usr/local/include + /opt/local/include + /sw/include + ${CMAKE_INCLUDE_PATH} + ${CMAKE_INSTALL_PREFIX}/include + ) + + find_library(LIBSSH_LIBRARY + NAMES + ssh + libssh + HINTS + "${LIBSSH_HINTS}/lib" + PATHS + /usr/lib + /usr/local/lib + /opt/local/lib + /sw/lib + ${CMAKE_LIBRARY_PATH} + ${CMAKE_INSTALL_PREFIX}/lib + ) + + if (LIBSSH_INCLUDE_DIR AND LIBSSH_LIBRARY) + set(LIBSSH_INCLUDE_DIRS + ${LIBSSH_INCLUDE_DIR} + ) + set(LIBSSH_LIBRARIES + ${LIBSSH_LIBRARY} + ) + + file(STRINGS ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h LIBSSH_VERSION_MAJOR + REGEX "#define[ ]+LIBSSH_VERSION_MAJOR[ ]+[0-9]+") + # Older versions of libssh like libssh-0.2 have LIBSSH_VERSION but not LIBSSH_VERSION_MAJOR + if (LIBSSH_VERSION_MAJOR) + string(REGEX MATCH "[0-9]+" LIBSSH_VERSION_MAJOR ${LIBSSH_VERSION_MAJOR}) + file(STRINGS ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h LIBSSH_VERSION_MINOR + REGEX "#define[ ]+LIBSSH_VERSION_MINOR[ ]+[0-9]+") + string(REGEX MATCH "[0-9]+" LIBSSH_VERSION_MINOR ${LIBSSH_VERSION_MINOR}) + file(STRINGS ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h LIBSSH_VERSION_PATCH + REGEX "#define[ ]+LIBSSH_VERSION_MICRO[ ]+[0-9]+") + string(REGEX MATCH "[0-9]+" LIBSSH_VERSION_PATCH ${LIBSSH_VERSION_PATCH}) + set(LIBSSH_VERSION ${LIBSSH_VERSION_MAJOR}.${LIBSSH_VERSION_MINOR}.${LIBSSH_VERSION_PATCH}) + endif () + endif () + + # handle the QUIETLY and REQUIRED arguments and set LIBSSH_FOUND to TRUE if + # all listed variables are TRUE and the requested version matches. + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(LIBSSH + REQUIRED_VARS LIBSSH_LIBRARIES LIBSSH_INCLUDE_DIRS LIBSSH_VERSION + VERSION_VAR LIBSSH_VERSION) + + if (WIN32) + set(LIBSSH_DLL_DIR "${LIBSSH_HINTS}/bin" + CACHE PATH "Path to libssh DLL" + ) + file(GLOB _libssh_dll RELATIVE "${LIBSSH_DLL_DIR}" + "${LIBSSH_DLL_DIR}/libssh.dll" + ) + set(LIBSSH_DLL ${_libssh_dll} + # We're storing filenames only. Should we use STRING instead? + CACHE FILEPATH "libssh DLL file name" + ) + mark_as_advanced(LIBSSH_DLL_DIR LIBSSH_DLL) + endif () + + # show the LIBSSH_INCLUDE_DIRS and LIBSSH_LIBRARIES variables only in the advanced view + mark_as_advanced(LIBSSH_INCLUDE_DIRS LIBSSH_LIBRARIES) + +endif () \ No newline at end of file diff --git a/cmake/Modules/GNUInstallDirs.cmake b/cmake/Modules/GNUInstallDirs.cmake new file mode 100644 index 0000000..e10eef4 --- /dev/null +++ b/cmake/Modules/GNUInstallDirs.cmake @@ -0,0 +1,383 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +GNUInstallDirs +-------------- + +Define GNU standard installation directories + +Provides install directory variables as defined by the +`GNU Coding Standards`_. + +.. _`GNU Coding Standards`: https://www.gnu.org/prep/standards/html_node/Directory-Variables.html + +Result Variables +^^^^^^^^^^^^^^^^ + +Inclusion of this module defines the following variables: + +``CMAKE_INSTALL_`` + + Destination for files of a given type. This value may be passed to + the ``DESTINATION`` options of :command:`install` commands for the + corresponding file type. + +``CMAKE_INSTALL_FULL_`` + + The absolute path generated from the corresponding ``CMAKE_INSTALL_`` + value. If the value is not already an absolute path, an absolute path + is constructed typically by prepending the value of the + :variable:`CMAKE_INSTALL_PREFIX` variable. However, there are some + `special cases`_ as documented below. + +where ```` is one of: + +``BINDIR`` + user executables (``bin``) +``SBINDIR`` + system admin executables (``sbin``) +``LIBEXECDIR`` + program executables (``libexec``) +``SYSCONFDIR`` + read-only single-machine data (``etc``) +``SHAREDSTATEDIR`` + modifiable architecture-independent data (``com``) +``LOCALSTATEDIR`` + modifiable single-machine data (``var``) +``RUNSTATEDIR`` + run-time variable data (``LOCALSTATEDIR/run``) +``LIBDIR`` + object code libraries (``lib`` or ``lib64`` + or ``lib/`` on Debian) +``INCLUDEDIR`` + C header files (``include``) +``OLDINCLUDEDIR`` + C header files for non-gcc (``/usr/include``) +``DATAROOTDIR`` + read-only architecture-independent data root (``share``) +``DATADIR`` + read-only architecture-independent data (``DATAROOTDIR``) +``INFODIR`` + info documentation (``DATAROOTDIR/info``) +``LOCALEDIR`` + locale-dependent data (``DATAROOTDIR/locale``) +``MANDIR`` + man documentation (``DATAROOTDIR/man``) +``DOCDIR`` + documentation root (``DATAROOTDIR/doc/PROJECT_NAME``) + +If the includer does not define a value the above-shown default will be +used and the value will appear in the cache for editing by the user. + +Special Cases +^^^^^^^^^^^^^ + +The following values of :variable:`CMAKE_INSTALL_PREFIX` are special: + +``/`` + + For ```` other than the ``SYSCONFDIR``, ``LOCALSTATEDIR`` and + ``RUNSTATEDIR``, the value of ``CMAKE_INSTALL_`` is prefixed + with ``usr/`` if it is not user-specified as an absolute path. + For example, the ``INCLUDEDIR`` value ``include`` becomes ``usr/include``. + This is required by the `GNU Coding Standards`_, which state: + + When building the complete GNU system, the prefix will be empty + and ``/usr`` will be a symbolic link to ``/``. + +``/usr`` + + For ```` equal to ``SYSCONFDIR``, ``LOCALSTATEDIR`` or + ``RUNSTATEDIR``, the ``CMAKE_INSTALL_FULL_`` is computed by + prepending just ``/`` to the value of ``CMAKE_INSTALL_`` + if it is not user-specified as an absolute path. + For example, the ``SYSCONFDIR`` value ``etc`` becomes ``/etc``. + This is required by the `GNU Coding Standards`_. + +``/opt/...`` + + For ```` equal to ``SYSCONFDIR``, ``LOCALSTATEDIR`` or + ``RUNSTATEDIR``, the ``CMAKE_INSTALL_FULL_`` is computed by + *appending* the prefix to the value of ``CMAKE_INSTALL_`` + if it is not user-specified as an absolute path. + For example, the ``SYSCONFDIR`` value ``etc`` becomes ``/etc/opt/...``. + This is defined by the `Filesystem Hierarchy Standard`_. + +.. _`Filesystem Hierarchy Standard`: https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html + +Macros +^^^^^^ + +.. command:: GNUInstallDirs_get_absolute_install_dir + + :: + + GNUInstallDirs_get_absolute_install_dir(absvar var) + + Set the given variable ``absvar`` to the absolute path contained + within the variable ``var``. This is to allow the computation of an + absolute path, accounting for all the special cases documented + above. While this macro is used to compute the various + ``CMAKE_INSTALL_FULL_`` variables, it is exposed publicly to + allow users who create additional path variables to also compute + absolute paths where necessary, using the same logic. +#]=======================================================================] + +cmake_policy(PUSH) +cmake_policy(SET CMP0054 NEW) # if() quoted variables not dereferenced + +# Convert a cache variable to PATH type + +macro(_GNUInstallDirs_cache_convert_to_path var description) + get_property(_GNUInstallDirs_cache_type CACHE ${var} PROPERTY TYPE) + if(_GNUInstallDirs_cache_type STREQUAL "UNINITIALIZED") + file(TO_CMAKE_PATH "${${var}}" _GNUInstallDirs_cmakepath) + set_property(CACHE ${var} PROPERTY TYPE PATH) + set_property(CACHE ${var} PROPERTY VALUE "${_GNUInstallDirs_cmakepath}") + set_property(CACHE ${var} PROPERTY HELPSTRING "${description}") + unset(_GNUInstallDirs_cmakepath) + endif() + unset(_GNUInstallDirs_cache_type) +endmacro() + +# Create a cache variable with default for a path. +macro(_GNUInstallDirs_cache_path var default description) + if(NOT DEFINED ${var}) + set(${var} "${default}" CACHE PATH "${description}") + endif() + _GNUInstallDirs_cache_convert_to_path("${var}" "${description}") +endmacro() + +# Create a cache variable with not default for a path, with a fallback +# when unset; used for entries slaved to other entries such as +# DATAROOTDIR. +macro(_GNUInstallDirs_cache_path_fallback var default description) + if(NOT ${var}) + set(${var} "" CACHE PATH "${description}") + set(${var} "${default}") + endif() + _GNUInstallDirs_cache_convert_to_path("${var}" "${description}") +endmacro() + +# Installation directories +# + +_GNUInstallDirs_cache_path(CMAKE_INSTALL_BINDIR "bin" + "User executables (bin)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_SBINDIR "sbin" + "System admin executables (sbin)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_LIBEXECDIR "libexec" + "Program executables (libexec)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_SYSCONFDIR "etc" + "Read-only single-machine data (etc)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_SHAREDSTATEDIR "com" + "Modifiable architecture-independent data (com)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_LOCALSTATEDIR "var" + "Modifiable single-machine data (var)") + +# We check if the variable was manually set and not cached, in order to +# allow projects to set the values as normal variables before including +# GNUInstallDirs to avoid having the entries cached or user-editable. It +# replaces the "if(NOT DEFINED CMAKE_INSTALL_XXX)" checks in all the +# other cases. +# If CMAKE_INSTALL_LIBDIR is defined, if _libdir_set is false, then the +# variable is a normal one, otherwise it is a cache one. +get_property(_libdir_set CACHE CMAKE_INSTALL_LIBDIR PROPERTY TYPE SET) +if(NOT DEFINED CMAKE_INSTALL_LIBDIR OR (_libdir_set + AND DEFINED _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX + AND NOT "${_GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX}" STREQUAL "${CMAKE_INSTALL_PREFIX}")) + # If CMAKE_INSTALL_LIBDIR is not defined, it is always executed. + # Otherwise: + # * if _libdir_set is false it is not executed (meaning that it is + # not a cache variable) + # * if _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX is not defined it is + # not executed + # * if _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX and + # CMAKE_INSTALL_PREFIX are the same string it is not executed. + # _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX is updated after the + # execution, of this part of code, therefore at the next inclusion + # of the file, CMAKE_INSTALL_LIBDIR is defined, and the 2 strings + # are equal, meaning that the if is not executed the code the + # second time. + + set(_LIBDIR_DEFAULT "lib") + # Override this default 'lib' with 'lib64' iff: + # - we are on Linux system but NOT cross-compiling + # - we are NOT on debian + # - we are on a 64 bits system + # reason is: amd64 ABI: https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI + # For Debian with multiarch, use 'lib/${CMAKE_LIBRARY_ARCHITECTURE}' if + # CMAKE_LIBRARY_ARCHITECTURE is set (which contains e.g. "i386-linux-gnu" + # and CMAKE_INSTALL_PREFIX is "/usr" + # See http://wiki.debian.org/Multiarch + if(DEFINED _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX) + set(__LAST_LIBDIR_DEFAULT "lib") + # __LAST_LIBDIR_DEFAULT is the default value that we compute from + # _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX, not a cache entry for + # the value that was last used as the default. + # This value is used to figure out whether the user changed the + # CMAKE_INSTALL_LIBDIR value manually, or if the value was the + # default one. When CMAKE_INSTALL_PREFIX changes, the value is + # updated to the new default, unless the user explicitly changed it. + endif() + if (NOT DEFINED CMAKE_SYSTEM_NAME OR NOT DEFINED CMAKE_SIZEOF_VOID_P) + message(AUTHOR_WARNING + "Unable to determine default CMAKE_INSTALL_LIBDIR directory because no target architecture is known. " + "Please enable at least one language before including GNUInstallDirs.") + endif() + if(CMAKE_SYSTEM_NAME MATCHES "^(Linux|kFreeBSD|GNU)$" + AND NOT CMAKE_CROSSCOMPILING + AND NOT EXISTS "/etc/arch-release") + if (EXISTS "/etc/debian_version") # is this a debian system ? + if(CMAKE_LIBRARY_ARCHITECTURE) + if("${CMAKE_INSTALL_PREFIX}" MATCHES "^/usr/?$") + set(_LIBDIR_DEFAULT "lib/${CMAKE_LIBRARY_ARCHITECTURE}") + endif() + if(DEFINED _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX + AND "${_GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX}" MATCHES "^/usr/?$") + set(__LAST_LIBDIR_DEFAULT "lib/${CMAKE_LIBRARY_ARCHITECTURE}") + endif() + endif() + else() # not debian, rely on CMAKE_SIZEOF_VOID_P: + if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + set(_LIBDIR_DEFAULT "lib64") + if(DEFINED _GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX) + set(__LAST_LIBDIR_DEFAULT "lib64") + endif() + endif() + endif() + endif() + if(NOT DEFINED CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR "${_LIBDIR_DEFAULT}" CACHE PATH "Object code libraries (${_LIBDIR_DEFAULT})") + elseif(DEFINED __LAST_LIBDIR_DEFAULT + AND "${__LAST_LIBDIR_DEFAULT}" STREQUAL "${CMAKE_INSTALL_LIBDIR}") + set_property(CACHE CMAKE_INSTALL_LIBDIR PROPERTY VALUE "${_LIBDIR_DEFAULT}") + endif() +endif() +_GNUInstallDirs_cache_convert_to_path(CMAKE_INSTALL_LIBDIR "Object code libraries (lib)") + +# Save for next run +set(_GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" CACHE INTERNAL "CMAKE_INSTALL_PREFIX during last run") +unset(_libdir_set) +unset(__LAST_LIBDIR_DEFAULT) + +_GNUInstallDirs_cache_path(CMAKE_INSTALL_INCLUDEDIR "include" + "C header files (include)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_OLDINCLUDEDIR "/usr/include" + "C header files for non-gcc (/usr/include)") +_GNUInstallDirs_cache_path(CMAKE_INSTALL_DATAROOTDIR "share" + "Read-only architecture-independent data root (share)") + +#----------------------------------------------------------------------------- +# Values whose defaults are relative to DATAROOTDIR. Store empty values in +# the cache and store the defaults in local variables if the cache values are +# not set explicitly. This auto-updates the defaults as DATAROOTDIR changes. + +_GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_DATADIR "${CMAKE_INSTALL_DATAROOTDIR}" + "Read-only architecture-independent data (DATAROOTDIR)") + +if(CMAKE_SYSTEM_NAME MATCHES "^(([^kF].*)?BSD|DragonFly)$") + _GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_INFODIR "info" + "Info documentation (info)") +else() + _GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_INFODIR "${CMAKE_INSTALL_DATAROOTDIR}/info" + "Info documentation (DATAROOTDIR/info)") +endif() + +if(CMAKE_SYSTEM_NAME MATCHES "^(([^k].*)?BSD|DragonFly)$") + _GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_MANDIR "man" + "Man documentation (man)") +else() + _GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_MANDIR "${CMAKE_INSTALL_DATAROOTDIR}/man" + "Man documentation (DATAROOTDIR/man)") +endif() + +_GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_LOCALEDIR "${CMAKE_INSTALL_DATAROOTDIR}/locale" + "Locale-dependent data (DATAROOTDIR/locale)") +_GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_DOCDIR "${CMAKE_INSTALL_DATAROOTDIR}/doc/${PROJECT_NAME}" + "Documentation root (DATAROOTDIR/doc/PROJECT_NAME)") + +_GNUInstallDirs_cache_path_fallback(CMAKE_INSTALL_RUNSTATEDIR "${CMAKE_INSTALL_LOCALSTATEDIR}/run" + "Run-time variable data (LOCALSTATEDIR/run)") + +#----------------------------------------------------------------------------- + +mark_as_advanced( + CMAKE_INSTALL_BINDIR + CMAKE_INSTALL_SBINDIR + CMAKE_INSTALL_LIBEXECDIR + CMAKE_INSTALL_SYSCONFDIR + CMAKE_INSTALL_SHAREDSTATEDIR + CMAKE_INSTALL_LOCALSTATEDIR + CMAKE_INSTALL_RUNSTATEDIR + CMAKE_INSTALL_LIBDIR + CMAKE_INSTALL_INCLUDEDIR + CMAKE_INSTALL_OLDINCLUDEDIR + CMAKE_INSTALL_DATAROOTDIR + CMAKE_INSTALL_DATADIR + CMAKE_INSTALL_INFODIR + CMAKE_INSTALL_LOCALEDIR + CMAKE_INSTALL_MANDIR + CMAKE_INSTALL_DOCDIR +) + +macro(GNUInstallDirs_get_absolute_install_dir absvar var) + if(NOT IS_ABSOLUTE "${${var}}") + # Handle special cases: + # - CMAKE_INSTALL_PREFIX == / + # - CMAKE_INSTALL_PREFIX == /usr + # - CMAKE_INSTALL_PREFIX == /opt/... + if("${CMAKE_INSTALL_PREFIX}" STREQUAL "/") + if("${dir}" STREQUAL "SYSCONFDIR" OR "${dir}" STREQUAL "LOCALSTATEDIR" OR "${dir}" STREQUAL "RUNSTATEDIR") + set(${absvar} "/${${var}}") + else() + if (NOT "${${var}}" MATCHES "^usr/") + set(${var} "usr/${${var}}") + endif() + set(${absvar} "/${${var}}") + endif() + elseif("${CMAKE_INSTALL_PREFIX}" MATCHES "^/usr/?$") + if("${dir}" STREQUAL "SYSCONFDIR" OR "${dir}" STREQUAL "LOCALSTATEDIR" OR "${dir}" STREQUAL "RUNSTATEDIR") + set(${absvar} "/${${var}}") + else() + set(${absvar} "${CMAKE_INSTALL_PREFIX}/${${var}}") + endif() + elseif("${CMAKE_INSTALL_PREFIX}" MATCHES "^/opt/.*") + if("${dir}" STREQUAL "SYSCONFDIR" OR "${dir}" STREQUAL "LOCALSTATEDIR" OR "${dir}" STREQUAL "RUNSTATEDIR") + set(${absvar} "/${${var}}${CMAKE_INSTALL_PREFIX}") + else() + set(${absvar} "${CMAKE_INSTALL_PREFIX}/${${var}}") + endif() + else() + set(${absvar} "${CMAKE_INSTALL_PREFIX}/${${var}}") + endif() + else() + set(${absvar} "${${var}}") + endif() +endmacro() + +# Result directories +# +foreach(dir + BINDIR + SBINDIR + LIBEXECDIR + SYSCONFDIR + SHAREDSTATEDIR + LOCALSTATEDIR + RUNSTATEDIR + LIBDIR + INCLUDEDIR + OLDINCLUDEDIR + DATAROOTDIR + DATADIR + INFODIR + LOCALEDIR + MANDIR + DOCDIR + ) + GNUInstallDirs_get_absolute_install_dir(CMAKE_INSTALL_FULL_${dir} CMAKE_INSTALL_${dir}) +endforeach() + +cmake_policy(POP) \ No newline at end of file diff --git a/dist/centos/7/Dockerfile b/dist/centos/7/Dockerfile new file mode 100644 index 0000000..015a5f2 --- /dev/null +++ b/dist/centos/7/Dockerfile @@ -0,0 +1,30 @@ +FROM centos:7 + +RUN rpm -i http://www.nosuchhost.net/~cheese/fedora/packages/epel-7/x86_64/cheese-release-7-1.noarch.rpm + +RUN yum -y update && \ + yum -y install centos-release-scl && \ + yum-config-manager --enable rhel-server-rhscl-7-rpms && \ + yum -y install \ + make \ + rpm-build \ + wget \ + llvm-toolset-7.0 \ + lua-devel \ + libcurl-devel \ + libarchive-devel \ + libssh-devel \ + libgit2-devel + +ENV CC /opt/rh/llvm-toolset-7.0/root/usr/bin/clang +ENV CXX /opt/rh/llvm-toolset-7.0/root/usr/bin/clang++ + +# Build requires at least CMake 3.11.4, CentOS 8 has CMake 2.8 +RUN mkdir -p /opt/cmake/ && \ + cd /opt/cmake && \ + wget https://github.com/Kitware/CMake/releases/download/v3.16.4/cmake-3.16.4-Linux-x86_64.tar.gz && \ + tar -xvf cmake-3.16.4-Linux-x86_64.tar.gz + +ENV PATH="/opt/cmake/cmake-3.16.4-Linux-x86_64/bin:${PATH}" + +CMD /kafe/dist/centos/7/build-dist.sh \ No newline at end of file diff --git a/dist/centos/7/build-dist.sh b/dist/centos/7/build-dist.sh new file mode 100755 index 0000000..89d3d57 --- /dev/null +++ b/dist/centos/7/build-dist.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Enable software collections LLVM toolset (7.0) +source scl_source enable llvm-toolset-7.0 + +set -e + +# Clean existing build workspace +rm -rf /kafe/build/centos/7 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/centos/7/ + +# Warp to build workspace +cd /kafe/build/centos/7/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=RPM ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +rpm -i ./kafe-cli*.rpm ./libkafe*.rpm +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/centos/8/Dockerfile b/dist/centos/8/Dockerfile new file mode 100644 index 0000000..0228719 --- /dev/null +++ b/dist/centos/8/Dockerfile @@ -0,0 +1,21 @@ +FROM centos:8 + +RUN yum -y update && \ + yum -y install dnf-plugins-core && \ + yum config-manager --set-enabled PowerTools && \ + yum -y install \ + make \ + rpm-build \ + wget \ + clang \ + cmake \ + lua-devel \ + libcurl-devel \ + libarchive-devel \ + libssh-devel \ + libgit2-devel + +ENV CC /usr/bin/clang-8 +ENV CXX /usr/bin/clang++-8 + +CMD /kafe/dist/centos/8/build-dist.sh \ No newline at end of file diff --git a/dist/centos/8/build-dist.sh b/dist/centos/8/build-dist.sh new file mode 100755 index 0000000..2b1c97a --- /dev/null +++ b/dist/centos/8/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/centos/8 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/centos/8/ + +# Warp to build workspace +cd /kafe/build/centos/8/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=RPM ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +rpm -i ./kafe-cli*.rpm ./libkafe*.rpm +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/debian/10/Dockerfile b/dist/debian/10/Dockerfile new file mode 100644 index 0000000..4a3cccb --- /dev/null +++ b/dist/debian/10/Dockerfile @@ -0,0 +1,19 @@ +FROM debian:10 + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -y \ + wget \ + cmake \ + build-essential \ + clang-7 \ + liblua5.3-dev \ + libcurl4-gnutls-dev \ + libarchive-dev \ + libssh-dev \ + libgit2-dev + +ENV CC /usr/bin/clang-7 +ENV CXX /usr/bin/clang++-7 + +CMD /kafe/dist/debian/10/build-dist.sh \ No newline at end of file diff --git a/dist/debian/10/build-dist.sh b/dist/debian/10/build-dist.sh new file mode 100755 index 0000000..6f15951 --- /dev/null +++ b/dist/debian/10/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/debian/10 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/debian/10/ + +# Warp to build workspace +cd /kafe/build/debian/10/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=DEB ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +apt-get install -y ./kafe-cli*.deb ./libkafe*.deb +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/debian/11/Dockerfile b/dist/debian/11/Dockerfile new file mode 100644 index 0000000..8e0e15c --- /dev/null +++ b/dist/debian/11/Dockerfile @@ -0,0 +1,19 @@ +FROM debian:bullseye + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -y \ + wget \ + cmake \ + build-essential \ + clang-9 \ + liblua5.3-dev \ + libcurl4-gnutls-dev \ + libarchive-dev \ + libssh-dev \ + libgit2-dev + +ENV CC /usr/bin/clang-9 +ENV CXX /usr/bin/clang++-9 + +CMD /kafe/dist/debian/11/build-dist.sh \ No newline at end of file diff --git a/dist/debian/11/build-dist.sh b/dist/debian/11/build-dist.sh new file mode 100755 index 0000000..097869f --- /dev/null +++ b/dist/debian/11/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/debian/11 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/debian/11/ + +# Warp to build workspace +cd /kafe/build/debian/11/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=DEB ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +apt-get install -y ./kafe-cli*.deb ./libkafe*.deb +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/debian/9/Dockerfile b/dist/debian/9/Dockerfile new file mode 100644 index 0000000..3e39648 --- /dev/null +++ b/dist/debian/9/Dockerfile @@ -0,0 +1,26 @@ +FROM debian:9 + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -y \ + wget \ + build-essential \ + clang-7 \ + liblua5.3-dev \ + libcurl4-gnutls-dev \ + libarchive-dev \ + libssh-dev \ + libgit2-dev + +ENV CC /usr/bin/clang-7 +ENV CXX /usr/bin/clang++-7 + +# Build requires at least CMake 3.11.4, Debian 9 has CMake 3.7 +RUN mkdir -p /opt/cmake/ && \ + cd /opt/cmake && \ + wget https://github.com/Kitware/CMake/releases/download/v3.16.4/cmake-3.16.4-Linux-x86_64.tar.gz && \ + tar -xvf cmake-3.16.4-Linux-x86_64.tar.gz + +ENV PATH="/opt/cmake/cmake-3.16.4-Linux-x86_64/bin:${PATH}" + +CMD /kafe/dist/debian/9/build-dist.sh \ No newline at end of file diff --git a/dist/debian/9/build-dist.sh b/dist/debian/9/build-dist.sh new file mode 100755 index 0000000..c16106c --- /dev/null +++ b/dist/debian/9/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/debian/9 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/debian/9/ + +# Warp to build workspace +cd /kafe/build/debian/9/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=DEB ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +apt-get install -y ./kafe-cli*.deb ./libkafe*.deb +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/fedora/31/Dockerfile b/dist/fedora/31/Dockerfile new file mode 100644 index 0000000..20c930e --- /dev/null +++ b/dist/fedora/31/Dockerfile @@ -0,0 +1,19 @@ +FROM fedora:31 + +RUN yum -y update && \ + yum -y install \ + make \ + rpm-build \ + wget \ + clang \ + cmake \ + lua-devel \ + libcurl-devel \ + libarchive-devel \ + libssh-devel \ + libgit2-devel + +ENV CC /usr/bin/clang-9 +ENV CXX /usr/bin/clang++-9 + +CMD /kafe/dist/fedora/31/build-dist.sh \ No newline at end of file diff --git a/dist/fedora/31/build-dist.sh b/dist/fedora/31/build-dist.sh new file mode 100755 index 0000000..f58959f --- /dev/null +++ b/dist/fedora/31/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/fedora/31 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/fedora/31/ + +# Warp to build workspace +cd /kafe/build/fedora/31/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=RPM ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +rpm -i ./kafe-cli*.rpm ./libkafe*.rpm +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/fedora/32/Dockerfile b/dist/fedora/32/Dockerfile new file mode 100644 index 0000000..72ab875 --- /dev/null +++ b/dist/fedora/32/Dockerfile @@ -0,0 +1,19 @@ +FROM fedora:32 + +RUN yum -y update && \ + yum -y install \ + make \ + rpm-build \ + wget \ + clang \ + cmake \ + lua-devel \ + libcurl-devel \ + libarchive-devel \ + libssh-devel \ + libgit2-devel + +ENV CC /usr/bin/clang-10 +ENV CXX /usr/bin/clang++-10 + +CMD /kafe/dist/fedora/32/build-dist.sh \ No newline at end of file diff --git a/dist/fedora/32/build-dist.sh b/dist/fedora/32/build-dist.sh new file mode 100755 index 0000000..361936e --- /dev/null +++ b/dist/fedora/32/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/fedora/32 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/fedora/32/ + +# Warp to build workspace +cd /kafe/build/fedora/32/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=RPM ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +rpm -i ./kafe-cli*.rpm ./libkafe*.rpm +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/fedora/33/Dockerfile b/dist/fedora/33/Dockerfile new file mode 100644 index 0000000..0bc3b30 --- /dev/null +++ b/dist/fedora/33/Dockerfile @@ -0,0 +1,19 @@ +FROM fedora:33 + +RUN yum -y update && \ + yum -y install \ + make \ + rpm-build \ + wget \ + clang \ + cmake \ + lua-devel \ + libcurl-devel \ + libarchive-devel \ + libssh-devel \ + libgit2-devel + +ENV CC /usr/bin/clang-10 +ENV CXX /usr/bin/clang++-10 + +CMD /kafe/dist/fedora/33/build-dist.sh \ No newline at end of file diff --git a/dist/fedora/33/build-dist.sh b/dist/fedora/33/build-dist.sh new file mode 100755 index 0000000..e5d8f24 --- /dev/null +++ b/dist/fedora/33/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/fedora/33 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/fedora/33/ + +# Warp to build workspace +cd /kafe/build/fedora/33/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=RPM ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +rpm -i ./kafe-cli*.rpm ./libkafe*.rpm +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/ubuntu/1804/Dockerfile b/dist/ubuntu/1804/Dockerfile new file mode 100644 index 0000000..eda0d18 --- /dev/null +++ b/dist/ubuntu/1804/Dockerfile @@ -0,0 +1,27 @@ +FROM ubuntu:18.04 + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -y \ + wget \ + cmake \ + build-essential \ + clang-9 \ + liblua5.3-dev \ + libcurl4-gnutls-dev \ + libarchive-dev \ + libssh-dev \ + libgit2-dev + +ENV CC /usr/bin/clang-9 +ENV CXX /usr/bin/clang++-9 + +# Build requires at least CMake 3.11.4, Ubuntu 18.04 has CMake 3.10 +RUN mkdir -p /opt/cmake/ && \ + cd /opt/cmake && \ + wget https://github.com/Kitware/CMake/releases/download/v3.16.4/cmake-3.16.4-Linux-x86_64.tar.gz && \ + tar -xvf cmake-3.16.4-Linux-x86_64.tar.gz + +ENV PATH="/opt/cmake/cmake-3.16.4-Linux-x86_64/bin:${PATH}" + +CMD /kafe/dist/ubuntu/1804/build-dist.sh \ No newline at end of file diff --git a/dist/ubuntu/1804/build-dist.sh b/dist/ubuntu/1804/build-dist.sh new file mode 100755 index 0000000..c01818c --- /dev/null +++ b/dist/ubuntu/1804/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/ubuntu/1804 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/ubuntu/1804/ + +# Warp to build workspace +cd /kafe/build/ubuntu/1804/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=DEB ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +apt-get install -y ./kafe-cli*.deb ./libkafe*.deb +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/ubuntu/1910/Dockerfile b/dist/ubuntu/1910/Dockerfile new file mode 100644 index 0000000..78b6126 --- /dev/null +++ b/dist/ubuntu/1910/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:19.10 + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -y \ + wget \ + cmake \ + build-essential \ + clang-9 \ + liblua5.3-dev \ + libcurl4-gnutls-dev \ + libarchive-dev \ + libssh-dev \ + libgit2-dev + +ENV CC /usr/bin/clang-9 +ENV CXX /usr/bin/clang++-9 + +CMD /kafe/dist/ubuntu/1910/build-dist.sh \ No newline at end of file diff --git a/dist/ubuntu/1910/build-dist.sh b/dist/ubuntu/1910/build-dist.sh new file mode 100755 index 0000000..620d534 --- /dev/null +++ b/dist/ubuntu/1910/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/ubuntu/1910 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/ubuntu/1910/ + +# Warp to build workspace +cd /kafe/build/ubuntu/1910/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=DEB ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +apt-get install -y ./kafe-cli*.deb ./libkafe*.deb +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/dist/ubuntu/2004/Dockerfile b/dist/ubuntu/2004/Dockerfile new file mode 100644 index 0000000..9fdaf6b --- /dev/null +++ b/dist/ubuntu/2004/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:20.04 + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y -y \ + wget \ + cmake \ + build-essential \ + clang-9 \ + liblua5.3-dev \ + libcurl4-gnutls-dev \ + libarchive-dev \ + libssh-dev \ + libgit2-dev + +ENV CC /usr/bin/clang-9 +ENV CXX /usr/bin/clang++-9 + +CMD /kafe/dist/ubuntu/2004/build-dist.sh \ No newline at end of file diff --git a/dist/ubuntu/2004/build-dist.sh b/dist/ubuntu/2004/build-dist.sh new file mode 100755 index 0000000..6834a8e --- /dev/null +++ b/dist/ubuntu/2004/build-dist.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Clean existing build workspace +rm -rf /kafe/build/ubuntu/2004 2> /dev/null + +# Create build workspace +mkdir -p /kafe/build/ubuntu/2004/ + +# Warp to build workspace +cd /kafe/build/ubuntu/2004/ + +# Prepare build +cmake -DCMAKE_BUILD_TYPE=Release -DCPACK_GENERATOR=DEB ../../../ + +# Compile project +make + +# Package project +cpack + +# Test install +apt-get install -y ./kafe-cli*.deb ./libkafe*.deb +env kafe about + +# Fix permissions +cd / + +WHO=/kafe/ + +stat $WHO > /dev/null || (echo You must mount a file to "$WHO" in order to properly assume user && exit 1) + +USERID=$(stat -c %u $WHO) +GROUPID=$(stat -c %g $WHO) + +if [ "$USERID" -eq "0" ]; then + USERID=1000 +fi + +if [ "$GROUPID" -eq "0" ]; then + GROUPID=1000 +fi + +chown -Rf $USERID:$GROUPID /kafe/build \ No newline at end of file diff --git a/docs/SCRIPTING_API_L1.md b/docs/SCRIPTING_API_L1.md new file mode 100644 index 0000000..5e343f7 --- /dev/null +++ b/docs/SCRIPTING_API_L1.md @@ -0,0 +1,233 @@ +# libkafe scripting API +## API compatibility level 1 + +This documentation assumes virtual module `kafe` is included as variable `k`, as demonstrated bellow: + +```lua +local k = require('kafe') +``` + +For future compatibility, all method calls are prefixed with this variable because more modules might be +introduced at some point. + +### Before using the scripting API + +Kafe scripting API is designed to be as low level or "native" as possible. Thus it is up to you to ensure +any shell commands you pass to the scripting API are safe to execute in their target hosts. + +The scripting API does not interact with remote privilege escalation tools (`su`, `sudo` etc.) - it is up to you to +escalate privileges as needed by using the native tools provided by the remote system. Remote prompts are not +supported at this time. One way to deal with privilege escalation is to use `sudo` command with +preconfigured sudo lines in `sudoers` configuration. You should only add the commands you intend to execute +to `sudoers` and never allow wildcard sudo commands to be executed without password prompt +(e.g. `ALL=(ALL:ALL) NOPASSWD: ALL`). Instead, try to design your remote automation as lean as root-less as possible. + +All command invocation is strictly sequential - there is no parallelization and parallel invocation of the +scripting API is not supported - even though you can achieve this in Lua. This is a conscious design decision. +Parallelization of remote management tasks might seem a good idea at first, but it rarely is - parallelization makes +remote error handling much more difficult, might leave remote systems in an inconsistent state and requires a +much larger investment in writing the automation tasks. In remote application deployment, parallelization is often used +to ensure all remote hosts receive deployments nearly at the same time. Parallelization is rarely a good solution +to this - you should split your tasks in groups of smaller tasks instead. For example, you can handle near same time deployment +by splitting up the tasks like this - upload all artifacts to all servers, unpack all artifacts on all servers, +symlink all the deployment on all servers, reload services on all servers. + +SSH connections to remote servers are reused - one remote server with unique combination of host, user and port will +only have one connection created regardless of environment and role. + +## Available API values + +#### string k.version + +Printable version of libkafe. + +#### int k.version_major + +Major version of libkafe. + +#### int k.version_minor + +Minor version of libkafe. + +#### int k.version_release + +Release version of libkafe. + +#### int k.api_level + +Current API level. + +## Available API methods + +### void k.require_api(int version) + +Require executing environment to be compatible to certain API compatibility level. + +Results in hard failure if: + +- API level of the environment is less than the one requested. + +### void k.task(string name, function callable) + +Define a task that can be invoked when the script is executed. + +Results in hard failure if: + +- Task with given name already exists. + +### void k.add_inventory(string user, string host, int port, string env, string role) + +Add a server to inventory of given environment with given role. + +Results in hard failure if: + +- Invalid port is provided (outside range 1-65535); or +- The same server is added to inventory again (duplicate). + +### bool k.on(string role, function callable [,bool skip_empty_inv = true]) + +Execute given function on each remote server with given role, in current environment. + +The third argument indicates if no remote servers available in current environment +and role combination should be considered an error. By default, libkafe will silently +skip over missing inventories. + +The return value indicates whether or not the the invocation succeeded. + +**IMPORTANT:** it is not possible to nest `.on(...)` invocations, e.g. you can not call `.on(...)` +when doing so results in calling another `.on(...)`. This behavior is not allowed and will result in hard failure. + +Results in hard failure if: + +- Called in a way that results in nested context; or +- If there are no servers in given inventory and `skip_empty_env` is not true. + +### void k.within(string directory_path) + +Execute all subsequent *remote* commands in given context within given directory. + +Effectively prepends `cd &&` to all subsequent shell commands in the same `k.on(...)` context. + +**IMPORTANT:** this command does not verify if remote directory exists or not. Invoking +subsequent remote shell commands in directory that does not exist will result in their +hard failure. + +### (string stdout, string stderr, int exit_code) k.exec(string command [, bool print_output = true]) + +Execute a remote shell command and return it's outputs along with exit code. + +Second optional argument indicates if the remote output should also be +logged in output of the tool. This option is enabled by default. + +### bool k.shell(string command) + +Execute a remote shell command, log output and return exit status as boolean. Will +return true if exit status of the remote command is `0`, false otherwise. + +### string k.archive_dir_tmp(string directory) + +Create a `.tar.gz` archive from given *local* directory and get the full path to the archive once created. +The resulting archive will be created in the temporary directory of the *local* machine. + +**IMPORTANT:** resulting archive will be deleted automatically once execution of the project is complete. + +Results in hard failure if: + +- Archive directory does not exist. + +### void k.archive_dir(string directory, string archive_file) + +Create a `.tar.gz` archive from given *local* directory and get the full path to the archive oncre created. +The resulting archive will be created in the path given as second argument on the *local* machine. + +Results in hard failure if: + +- Archive directory does not exist; or +- File or directory exists at the path provided in `archive_file`. + +### bool k.upload_file(string local_file, string remote_file) + +Upload local file to remote server in given path. `remote_file` can be +a file or directory. + +This command returns true if upload succeeded, and false on failure. + +**IMPORTANT:** remote directory to upload to must exist prior to upload. + +**IMPORTANT:** any existing remote files will be silently overwritten. + +### bool k.download_file(string local_file, string remote_file) + +Download remote file from remote server to given local path. + +This command returns true if download succeeded, and false on failure. + +**IMPORTANT:** any existing local files will be silently overwritten. + +### bool k.upload_str(string content, string remote_file) + +Upload text as file to remote server in given path. `remote_file` must be valid file. + +This command returns true if upload succeeded, and false on failure. + +**IMPORTANT:** remote directory to upload to must exist prior to upload. + +**IMPORTANT:** any existing remote files will be silently overwritten. + +### string|bool k.download_str(string remote_file) + +Download remote file from remote server as string. + +This command returns content of the file as string if download succeeded, and false on failure. + +## void k.define(string key, any value) + +Define a runtime variable in context of the executing script. These +values can be used for string templating in all commands that receive +string values, including `define` itself. + +Accepts any value that can be cast to string as second argument. + +**IMPORTANT:** keys are case sensitive. + +**IMPORTANT:** existing values using the same key will be silently overwritten. + +## string k.strfvars(string text) + +Replace any placeholder values of format `{{key}}` to their values as defined +using `k.define(...)`. + +**IMPORTANT:** keys are case sensitive. + +**IMPORTANT:** non-existent values will be replaced with empty string. + +## string k.strfenv(string text) + +Replace any placeholder values of format `{{key}}` to their values from +executing environment variables. + +**IMPORTANT:** keys are case sensitive. + +**IMPORTANT:** non-existent values will be replaced with empty string. + +## (string stdout, int code) k.local_exec(string command [, bool print_output = true]) + +Execute local shell command and return it's stdout and exit code. + +Second optional argument indicates if the remote output should also be +logged in output of the tool. This option is enabled by default. + +## bool k.local_shell(string command) + +Execute a local shell command, log output and return exit status as boolean. Will +return true if exit status of the local command is `0`, false otherwise. + +## void k.local_within(string directory_path) + +Execute all subsequent *local* commands within given directory, regardless of the remote context. + +Results in hard failure if: + +- Given directory does not exist. + + diff --git a/examples/deploy.lua b/examples/deploy.lua new file mode 100644 index 0000000..439c7ef --- /dev/null +++ b/examples/deploy.lua @@ -0,0 +1,89 @@ +--[[ + WARNING: DO NOT copy paste this code and execute verbatim. This script is an example and is not meant to be copied + and used without modification. +--]] + +-- Place the contents of this file in a file named kafe.lua in the root of your project. +-- Edit as needed. + +local k = require('kafe') +-- Ensure the kafe runtime has support for the API level this script requires. +k.require_api(1) + +-- Add remote servers to inventory. +-- Arguments: username, hostname/ip, port, environment, role. +k.add_inventory('john', 'one.example.org', 22, 'production', 'example_app') +k.add_inventory('john', 'two.example.org', 22, 'production', 'example_app') +k.add_inventory('john', 'stage.example.org', 22, 'staging', 'example_app') + +-- Define a single isolated task. +k.task('deploy', function() + -- Change local working directory. + k.local_within("~/software/") + -- Create an archive from folder as a temporary file. + -- You can provide absolute path or relative path to current working directory + -- of the process or one defined with `local_within`. + local archive = k.archive_dir_tmp('./example') + + local deploy = function() + -- Define some runtime properties. + k.define('deploy_to', '/opt/example_app/') + k.define('version', os.time(os.date('!*t'))) + + -- Create remote directory to deploy to. + -- Names inside double curly brackets will be replaced with values previously set with `define`. + -- Same applies to all calls against Kafe API with text arguments. + if not k.shell('mkdir -p {{deploy_to}}') + then error('Could not create deployment directory target') end + + -- Change remote directory to deployment target directory. + k.within('{{deploy_to}}') + + -- Create release path if it does not exist. + if not k.shell('mkdir -p releases/{{version}}') + then error('Could not create release root directory') end + + -- Upload our example application as an archive. + k.upload_file(archive, 'releases/{{version}}/upload.tar.gz') + + -- Unpack our example application from the archive. + if not k.shell('tar -xvf releases/{{version}}/upload.tar.gz -C releases/{{version}}') + then error('Could not unpack uploaded archive') end + + -- Remove the archive file (we don't need it any longer). + if not k.shell('rm releases/{{version}}/upload.tar.gz') + then error('Failed to remove uploaded archive') end + + -- Useful: allow group users to deal with our freshly uploaded content + if not k.shell('chmod g+rw releases && chmod g+rw -Rf releases/') + then error('Failed to fix chmod') end + end + + local symlink = function() + -- Change remote directory to deployment target directory. + k.within('{{deploy_to}}') + + -- Symlink the new version of the application to {{deploy_to}}/current. + if not k.shell('ln -nsfv releases/{{version}}/ current') + then error('Failed to update the symlink to the new version') end + end + + local reload_service = function() + -- Reload the service + if not k.shell('sudo systemctl reload example') + then error('Failed to reload the service') end + end + + -- Upload and unpack the new version + if not k.on('example_app', deploy) + then error('Failed to deploy') end + + -- Symlink the new version + if not k.on('example_app', symlink) + then error('Failed to symlink') end + + -- Reload the example service to apply the new version + if not k.on('example_app', reload_service) + then error('Failed to reload the service') end + +end \ No newline at end of file diff --git a/examples/deploy_from_git.lua b/examples/deploy_from_git.lua new file mode 100644 index 0000000..67490f0 --- /dev/null +++ b/examples/deploy_from_git.lua @@ -0,0 +1,110 @@ +--[[ + WARNING: DO NOT copy paste this code and execute verbatim. This script is an example and is not meant to be copied + and used without modification. +--]] + +-- Place the contents of this file in a file named kafe.lua in the root of your project. +-- Edit as needed. + +local k = require('kafe') +-- Ensure the kafe runtime has support for the API level this script requires. +k.require_api(1) + +-- Add remote servers to inventory. +-- Arguments: username, hostname/ip, port, environment, role. +k.add_inventory('john', 'one.example.org', 22, 'production', 'example_app') +k.add_inventory('john', 'two.example.org', 22, 'production', 'example_app') +k.add_inventory('john', 'stage.example.org', 22, 'staging', 'example_app') + +-- Define a single isolated task. +k.task('deploy', function() + local version = os.time(os.date('!*t')) -- Unix timestamp in seconds + + k.define('version', version) + k.define('deploy_to', '/opt/example_app/') + k.define('local_workspace', '/tmp/example_' .. version) + k.define('repo', 'GIT_REPO_URL') + + -- Create local workspace directory and parent directories if needed + if not k.local_shell('mkdir -p {{local_workspace}}') + then error('Could not create workspace') end + + -- Clone the remote repository + if not k.local_shell('git clone {{repo}} {{local_workspace}}') + then error('Could not clone repository') end + + -- Create an archive from sources + local archive = k.archive_dir_tmp('{{local_workspace}}') + + local deploy = function() + -- Create remote directory to deploy to. + -- Names inside double curly brackets will be replaced with values previously set with `define`. + -- Same applies to all calls against Kafe API with text arguments. + if not k.shell('mkdir -p {{deploy_to}}') + then error('Could not create deployment directory target') end + + -- Change remote directory to deployment target directory. + k.within('{{deploy_to}}') + + -- Create release path if it does not exist. + if not k.shell('mkdir -p releases/{{version}}') + then error('Could not create release root directory') end + + -- Upload our example application as an archive. + k.upload_file(archive, 'releases/{{version}}/upload.tar.gz') + + -- Unpack our example application from the archive. + if not k.shell('tar -xvf releases/{{version}}/upload.tar.gz -C releases/{{version}}') + then error('Could not unpack uploaded archive') end + + -- Remove the archive file (we don't need it any longer). + if not k.shell('rm releases/{{version}}/upload.tar.gz') + then error('Failed to remove uploaded archive') end + + -- Useful: allow group users to deal with our freshly uploaded content + if not k.shell('chmod g+rw releases && chmod g+rw -Rf releases/') + then error('Failed to fix chmod') end + end + + local symlink = function() + -- Change remote directory to deployment target directory. + k.within('{{deploy_to}}') + + -- Symlink the new version of the application to {{deploy_to}}/current. + if not k.shell('ln -nsfv releases/{{version}}/ current') + then error('Failed to update the symlink to the new version') end + end + + local reload_service = function() + -- Reload the service + if not k.shell('sudo systemctl reload example') + then error('Failed to reload the service') end + end + + local remove_old_releases = function() + -- Remove all but last 5 releases by deployment time + k.within('{{deploy_to}}/releases/') + + if not k.shell('ls -1tr | head -n -5 | xargs -d \'\\n\' rm -rf --') then + error('Failed to remove old releases') end + end + + -- Upload and unpack the new version, and get status of the invocation + local did_deploy = not k.on('example_app', deploy) + + -- Clean up local workspace + if not k.local_shell('rm -rf {{local_workspace}}') + then error('Could delete local workspace') end + + -- Discontinue if deployment task failed (no symlink or reload of the service needed) + if not did_deploy then error('Failed to deploy') end + + -- Symlink the new version + if not k.on('example_app', symlink) + then error('Failed to symlink') end + + -- Reload the example service to apply the new version + if not k.on('example_app', reload_service) + then error('Failed to reload the service') end + +end \ No newline at end of file diff --git a/libkafe/CMakeLists.txt b/libkafe/CMakeLists.txt new file mode 100644 index 0000000..e0d4d9e --- /dev/null +++ b/libkafe/CMakeLists.txt @@ -0,0 +1,162 @@ +cmake_minimum_required(VERSION 3.11.4) +project(kafe VERSION ${KAFE_VERSION} LANGUAGES CXX C) + +# Important: must be update for breaking scripting API changes +set(KAFE_API_LEVEL 1) + +message(STATUS "libkafe version is defined as ${PROJECT_VERSION}") + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(FIND_LIBRARY_USE_LIB64_PATHS ON) + +if (NOT UNIX AND NOT APPLE) + message(FATAL_ERROR "Not Unix or Apple, your operating system is not supported!") +endif () + +if(APPLE) + set(CMAKE_PREFIX_PATH "/usr/local/opt/libarchive/;/usr/local/opt/libssh/;/usr/local/opt/curl/;/usr/local/opt/libgit2/;/usr/local/opt/lua/") +endif() + +if (CMAKE_BUILD_TYPE MATCHES Debug) + message(STATUS "CMAKE IN DEBUG MODE") +elseif (CMAKE_BUILD_TYPE MATCHES Release) + message(STATUS "CMAKE IN RELEASE MODE") +endif () + +find_package(Lua 5.3 REQUIRED) +find_package(LIBSSH 0.7 REQUIRED) +find_package(LibArchive 3.1.2 REQUIRED) +find_package(CURL 7.11 REQUIRED) +find_package(LIBGIT2 REQUIRED) +find_package(Filesystem COMPONENTS Experimental Final REQUIRED) + +file(GLOB_RECURSE _HEADERS "include/*.hpp") +file(GLOB_RECURSE _SOURCES "src/*.[hc]pp") + +add_library(kafe_lib_shared SHARED ${_HEADERS} ${_SOURCES}) +add_library(kafe_lib_static STATIC ${_HEADERS} ${_SOURCES}) + +set_target_properties(kafe_lib_shared PROPERTIES OUTPUT_NAME "kafe") +set_target_properties(kafe_lib_static PROPERTIES OUTPUT_NAME "kafe") + +if (LUA_VERSION_MAJOR EQUAL 5 AND LUA_VERSION_MINOR EQUAL 4) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_LUA_54) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_LUA_54) + message(STATUS "Building with Lua 5.4") +elseif (LUA_VERSION_MAJOR EQUAL 5 AND LUA_VERSION_MINOR EQUAL 3) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_LUA_53) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_LUA_53) + message(STATUS "Building with Lua 5.3") +else () + message(FATAL_ERROR "Unsupported Lua version") +endif () + +target_link_libraries(kafe_lib_shared LINK_PRIVATE ${LUA_LIBRARIES}) +target_link_libraries(kafe_lib_static LINK_PRIVATE ${LUA_LIBRARIES}) + +target_link_libraries(kafe_lib_shared LINK_PRIVATE ${LIBSSH_LIBRARIES}) +target_link_libraries(kafe_lib_static LINK_PRIVATE ${LIBSSH_LIBRARIES}) + +target_link_libraries(kafe_lib_shared LINK_PRIVATE ${LibArchive_LIBRARIES}) +target_link_libraries(kafe_lib_static LINK_PRIVATE ${LibArchive_LIBRARIES}) + +target_link_libraries(kafe_lib_shared LINK_PRIVATE ${CURL_LIBRARIES}) +target_link_libraries(kafe_lib_static LINK_PRIVATE ${CURL_LIBRARIES}) + +target_link_libraries(kafe_lib_shared LINK_PRIVATE ${LIBGIT2_LIBRARIES}) +target_link_libraries(kafe_lib_static LINK_PRIVATE ${LIBGIT2_LIBRARIES}) + +target_link_libraries(kafe_lib_shared LINK_PRIVATE std::filesystem) +target_link_libraries(kafe_lib_static LINK_PRIVATE std::filesystem) + +target_include_directories(kafe_lib_shared PUBLIC include) +target_include_directories(kafe_lib_static PUBLIC include) + +target_include_directories(kafe_lib_shared PRIVATE ${LUA_INCLUDE_DIR}) +target_include_directories(kafe_lib_static PRIVATE ${LUA_INCLUDE_DIR}) + +target_include_directories(kafe_lib_shared PRIVATE ${LIBSSH_INCLUDE_DIR}) +target_include_directories(kafe_lib_static PRIVATE ${LIBSSH_INCLUDE_DIR}) + +target_include_directories(kafe_lib_shared PRIVATE ${LibArchive_INCLUDE_DIRS}) +target_include_directories(kafe_lib_static PRIVATE ${LibArchive_INCLUDE_DIRS}) + +target_include_directories(kafe_lib_shared PRIVATE ${CURL_INCLUDE_DIRS}) +target_include_directories(kafe_lib_static PRIVATE ${CURL_INCLUDE_DIRS}) + +target_include_directories(kafe_lib_shared PRIVATE ${LIBGIT2_INCLUDE_DIR}) +target_include_directories(kafe_lib_static PRIVATE ${LIBGIT2_INCLUDE_DIR}) + +string(TIMESTAMP KAFE_CMAKE_LIB_BUILD_TS "%Y-%m-%d" UTC) +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_BUILD_TS="${KAFE_CMAKE_LIB_BUILD_TS}") +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_BUILD_TS="${KAFE_CMAKE_LIB_BUILD_TS}") + +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_VERSION="${PROJECT_VERSION}") +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_VERSION="${PROJECT_VERSION}") + +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_API_VERSION=${KAFE_API_LEVEL}) +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_API_VERSION=${KAFE_API_LEVEL}) + +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_VERSION_INT=${KAFE_VERSION_INT}) +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_VERSION_INT=${KAFE_VERSION_INT}) + +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}) +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}) + +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_VERSION_MINOR=${PROJECT_VERSION_MINOR}) +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_VERSION_MINOR=${PROJECT_VERSION_MINOR}) + +target_compile_definitions(kafe_lib_shared PUBLIC KAFE_CMAKE_LIB_VERSION_RELEASE=${PROJECT_VERSION_PATCH}) +target_compile_definitions(kafe_lib_static PUBLIC KAFE_CMAKE_LIB_VERSION_RELEASE=${PROJECT_VERSION_PATCH}) + +find_path(KAFE_CMAKE_LIB_LUA_54 lua5.4/lua.h PATHS ${LUA_INCLUDE_DIR}) +find_path(KAFE_CMAKE_LIB_LUA_53 lua5.3/lua.h PATHS ${LUA_INCLUDE_DIR}) +find_path(KAFE_CMAKE_LIB_LUA_ROOT lua.h PATHS ${LUA_INCLUDE_DIR}) + +if (KAFE_CMAKE_LIB_LUA_54) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_LUA_54_VERSIONED) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_LUA_54_VERSIONED) + + target_include_directories(kafe_lib_shared PRIVATE ${KAFE_CMAKE_LIB_LUA_54}) + target_include_directories(kafe_lib_static PRIVATE ${KAFE_CMAKE_LIB_LUA_54}) + + message(STATUS "Lua 5.4 referred to by versioned include path") +elseif (KAFE_CMAKE_LIB_LUA_53) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_LUA_53_VERSIONED) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_LUA_53_VERSIONED) + + target_include_directories(kafe_lib_shared PRIVATE ${KAFE_CMAKE_LIB_LUA_53}) + target_include_directories(kafe_lib_static PRIVATE ${KAFE_CMAKE_LIB_LUA_53}) + + message(STATUS "Lua 5.3 referred to by versioned include path") +elseif (KAFE_CMAKE_LIB_LUA_ROOT) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_LUA_ROOT) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_LUA_ROOT) + + target_include_directories(kafe_lib_shared PRIVATE ${KAFE_CMAKE_LIB_LUA_ROOT}) + target_include_directories(kafe_lib_static PRIVATE ${KAFE_CMAKE_LIB_LUA_ROOT}) + + message(STATUS "Lua referred to by root include path") +else () + message(FATAL_ERROR "Could not determine Lua header to include. Expected either or ") +endif () + +if (_CXX_FILESYSTEM_HAVE_HEADER) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_FILESYSTEM) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_FILESYSTEM) +elseif (_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER) + target_compile_definitions(kafe_lib_shared PUBLIC KAFE_WITH_FILESYSTEM_EXPERIMENTAL) + target_compile_definitions(kafe_lib_static PUBLIC KAFE_WITH_FILESYSTEM_EXPERIMENTAL) +else () + message(FATAL_ERROR "No filesystem library") +endif () + +include(GNUInstallDirs) +install(TARGETS kafe_lib_shared DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libkafe) +install(TARGETS kafe_lib_static DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libkafe-dev) +install(FILES ${_HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/kafe COMPONENT libkafe-dev) diff --git a/libkafe/include/kafe/context.hpp b/libkafe/include/kafe/context.hpp new file mode 100644 index 0000000..4cce1df --- /dev/null +++ b/libkafe/include/kafe/context.hpp @@ -0,0 +1,56 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_CONTEXT_HPP +#define LIBKAFE_CONTEXT_HPP + +#include +#include +#include +#include "logging.hpp" + +using namespace std; + +namespace kafe { + class Context { + const map &envvals; + const string environment; + const vector tasks; + const ILogEventListener *log_listener; + public: + Context( + const map &envvals, + string environment, + vector tasks, + const ILogEventListener *log_listener + ); + + virtual ~Context(); + + [[nodiscard]] const map &get_envvals() const; + + [[nodiscard]] const string &get_environment() const; + + [[nodiscard]] const vector &get_tasks() const; + + [[nodiscard]] const ILogEventListener *get_log_listener() const; + }; +} + +#endif \ No newline at end of file diff --git a/libkafe/include/kafe/execution_scope.hpp b/libkafe/include/kafe/execution_scope.hpp new file mode 100644 index 0000000..06fc414 --- /dev/null +++ b/libkafe/include/kafe/execution_scope.hpp @@ -0,0 +1,85 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_EXECUTION_SCOPE_HPP +#define LIBKAFE_EXECUTION_SCOPE_HPP + +#include "kafe/remote/ssh_api.hpp" +#include "kafe/context.hpp" +#include "kafe/project/inventory.hpp" +#include "kafe/project/tasks.hpp" +#include "kafe/remote/ssh_pool.hpp" +#include "kafe/remote/ssh_api.hpp" +#include "kafe/local/local_api.hpp" + +using namespace std; +using namespace kafe; +using namespace kafe::project; +using namespace kafe::remote; +using namespace kafe::local; + +namespace kafe { + class ExecutionScope { + const Context &context; + const Inventory &inventory; + TaskList *tasks; + SshPool *ssh_pool; + SshApi *current_api = nullptr; + vector files_to_rm_on_destruct; + map values; + LocalApi *local; + public: + explicit ExecutionScope( + const Context &context, + const Inventory &inventory + ); + + [[nodiscard]] const Context *get_context() const; + + [[nodiscard]] const Inventory *get_inventory() const; + + [[nodiscard]] const TaskList *get_tasks() const; + + [[nodiscard]] const SshPool *get_ssh_pool() const; + + void set_current_remote(SshApi *api); + + [[nodiscard]] const SshApi *get_current_api() const; + + [[nodiscard]] bool has_current_api() const; + + void clear_current_api(); + + void add_rm_on_destruct(const string &file); + + virtual ~ExecutionScope(); + + void define_var(const string &key, const string &value); + + [[nodiscard]] const map &get_vars() const; + + [[nodiscard]] string replace_vars(const string &input) const; + + [[nodiscard]] string replace_env(const string &input) const; + + [[nodiscard]] LocalApi *get_local_api() const; + }; +} + +#endif \ No newline at end of file diff --git a/libkafe/include/kafe/io/archive.hpp b/libkafe/include/kafe/io/archive.hpp new file mode 100644 index 0000000..d94ef9c --- /dev/null +++ b/libkafe/include/kafe/io/archive.hpp @@ -0,0 +1,41 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_IO_ARCHIVE_HPP +#define LIBKAFE_IO_ARCHIVE_HPP + +extern "C" { +#include +#include +} + +#include + +using namespace std; + +namespace kafe::io { + class Archive { + public: + static string tmp_archive_from_directory(const string &directory); + + static void archive_from_directory(const string &archive_path, const string &directory); + }; +} + +#endif diff --git a/libkafe/include/kafe/io/file_system.hpp b/libkafe/include/kafe/io/file_system.hpp new file mode 100644 index 0000000..14a8f88 --- /dev/null +++ b/libkafe/include/kafe/io/file_system.hpp @@ -0,0 +1,65 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_IO_FILE_SYSTEM_HPP +#define LIBKAFE_IO_FILE_SYSTEM_HPP + +#ifdef KAFE_WITH_FILESYSTEM +#include + +namespace kafe::io { + namespace std_fs = std::filesystem; +} + +#elif defined(KAFE_WITH_FILESYSTEM_EXPERIMENTAL) + +#include + +namespace kafe::io { + namespace std_fs = std::experimental::filesystem; +} +#else +#error "No filesystem library" +#endif + +using namespace std; +using namespace kafe; + +namespace kafe::io { + class FileSystem { + public: + static bool is_file_or_symlink(const string &path_s); + + static bool is_directory(const string &path_s); + + static bool exists(const string &path); + + static bool mkdirs(const string &path); + + static void try_rm_r(const string &path); + + static std_fs::path absolute(const std_fs::path &p, const std_fs::path &base); + + static std_fs::path expand(const std_fs::path &p); + + static std_fs::path normalize(const std_fs::path &p, const std_fs::path &base); + }; +} + +#endif \ No newline at end of file diff --git a/libkafe/include/kafe/io/tty.hpp b/libkafe/include/kafe/io/tty.hpp new file mode 100644 index 0000000..8c7fbee --- /dev/null +++ b/libkafe/include/kafe/io/tty.hpp @@ -0,0 +1,31 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_IO_ANSI_COLORS_HPP +#define LIBKAFE_IO_ANSI_COLORS_HPP + +#define IO_TTY_ANSI_COLOR_RED "\x1b[31m" +#define IO_TTY_ANSI_COLOR_GREEN "\x1b[32m" +#define IO_TTY_ANSI_COLOR_YELLOW "\x1b[33m" +#define IO_TTY_ANSI_COLOR_BLUE "\x1b[34m" +#define IO_TTY_ANSI_COLOR_MAGENTA "\x1b[35m" +#define IO_TTY_ANSI_COLOR_CYAN "\x1b[36m" +#define IO_TTY_ANSI_COLOR_RESET "\x1b[0m" + +#endif diff --git a/libkafe/include/kafe/local/local_api.hpp b/libkafe/include/kafe/local/local_api.hpp new file mode 100644 index 0000000..cdb00f4 --- /dev/null +++ b/libkafe/include/kafe/local/local_api.hpp @@ -0,0 +1,58 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_LOCAL_LOCAL_API_HPP +#define LIBKAFE_LOCAL_LOCAL_API_HPP + +#include +#include "kafe/logging.hpp" + +using namespace std; +using namespace kafe; + +namespace kafe::local { + class LocalShellResult { + const string out; + const int code; + + public: + LocalShellResult(string out, int code); + + [[nodiscard]] string get_out() const; + + [[nodiscard]] int get_code() const; + }; + + class LocalApi { + const ILogEventListener *log_listener; + string current_chdir; + private: + string read_out(FILE *pFile, bool print_output); + + public: + explicit LocalApi(const ILogEventListener *log_listener); + + LocalShellResult local_popen(const string &command, bool print_output); + + void chdir(const string &chdir); + + [[nodiscard]] const string &get_chdir() const; + }; +} +#endif diff --git a/libkafe/include/kafe/logging.hpp b/libkafe/include/kafe/logging.hpp new file mode 100644 index 0000000..c802904 --- /dev/null +++ b/libkafe/include/kafe/logging.hpp @@ -0,0 +1,293 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "performance-unnecessary-value-param" +#ifndef LIBKAFE_LOGGING_HPP +#define LIBKAFE_LOGGING_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace kafe { + enum LogLevel { + ALL = 0, + TRACE = 1, + DEBUG = 2, + INFO = 3, + SUCCESS = 4, + WARNING = 5, + ERROR = 6, + NONE = 7 + }; + + class LoggingTimer { + private: + chrono::time_point t_start; + chrono::time_point t_end; + public: + LoggingTimer() { + this->t_start = chrono::system_clock::now(); + this->t_end = t_start; + } + + bool is_stopped() { + return t_start != t_end; + } + + LoggingTimer stop() { + if (is_stopped()) { + return *this; + } + t_end = chrono::system_clock::now(); + return *this; + } + + [[nodiscard]] string duration() const { + auto micros = chrono::duration_cast(t_end - t_start); + auto duration_micros = micros.count(); + stringstream buffer; + + string unit; + double duration; + if (duration_micros < 1000) { + unit = "μs"; + duration = (double) duration_micros; + } else if (duration_micros < 100000) { + unit = "ms"; + duration = (double) duration_micros / 1000; + } else if (duration_micros < 1000000) { + unit = "s"; + duration = (double) duration_micros / 1000 / 1000; + } + + buffer << duration << unit; + return buffer.str(); + } + }; + + class LogEvent { + const LogLevel level; + const string message; + chrono::time_point event_time; + const LoggingTimer *timer; + public: + LogEvent( + const LogLevel level, + string message, + const LoggingTimer *timer + ) : level(level), message(move(message)), timer(timer) { + this->event_time = chrono::system_clock::now(); + } + + [[nodiscard]] LogLevel get_level() const { + return level; + } + + [[nodiscard]] const string &get_message() const { + return message; + } + + [[nodiscard]] chrono::time_point get_event_time() const { + return event_time; + } + + [[nodiscard]] const LoggingTimer *get_timer() const { + return timer; + } + }; + +#define log_at_level \ + if (level < get_level()) { return; } \ + va_list args; \ + va_start(args, format); \ + char *buf; \ + vasprintf(&buf, format.c_str(), args); \ + va_end(args); \ + auto event = LogEvent(level, buf, nullptr); \ + on_log(event); \ + free(buf); + +#define log_at_level_it \ + if (nullptr != timer && !timer->is_stopped()) { timer->stop(); } \ + if (level < get_level()) { return; } \ + va_list args; \ + va_start(args, format); \ + char *buf; \ + vasprintf(&buf, format.c_str(), args); \ + va_end(args); \ + auto event = LogEvent(level, buf, timer); \ + on_log(event); \ + free(buf); + +#define log_at_level_wt \ + if (level < get_level()) { return timer(); } \ + va_list args; \ + va_start(args, format); \ + char *buf; \ + vasprintf(&buf, format.c_str(), args); \ + va_end(args); \ + auto event = LogEvent(level, buf, nullptr); \ + on_log(event); \ + free(buf); \ + return timer(); + + class ILogEventListener { + vector context = {}; + + protected: + virtual void on_log(const LogEvent &event) const = 0; + + public: + [[nodiscard]] vector get_context() const { + return context; + } + + public: + [[nodiscard]] LoggingTimer timer() const { + return LoggingTimer(); + } + + void context_push(const string &ctx) { + this->context.push_back(ctx); + } + + void context_pop() { + if (this->context.empty()) { + return; + } + this->context.pop_back(); + } + + void emit_trace(const string format, ...) const { + auto level = LogLevel::TRACE; + log_at_level + } + + void emit_trace(LoggingTimer *timer, const string format, ...) const { + auto level = LogLevel::TRACE; + log_at_level_it + } + + LoggingTimer emit_trace_wt(const string format, ...) const { + auto level = LogLevel::TRACE; + log_at_level_wt + } + + void emit_debug(const string format, ...) const { + auto level = LogLevel::DEBUG; + log_at_level + } + + void emit_debug(LoggingTimer *timer, const string format, ...) const { + auto level = LogLevel::DEBUG; + log_at_level_it + } + + LoggingTimer emit_debug_wt(const string format, ...) const { + auto level = LogLevel::DEBUG; + log_at_level_wt + } + + void emit_info(const string format, ...) const { + auto level = LogLevel::INFO; + log_at_level + } + + void emit_info(LoggingTimer *timer, const string format, ...) const { + auto level = LogLevel::INFO; + log_at_level_it + } + + LoggingTimer emit_info_wt(const string format, ...) const { + auto level = LogLevel::INFO; + log_at_level_wt + } + + void emit_success(const string format, ...) const { + auto level = LogLevel::SUCCESS; + log_at_level + } + + void emit_success(LoggingTimer *timer, const string format, ...) const { + auto level = LogLevel::SUCCESS; + log_at_level_it + } + + LoggingTimer emit_success_wt(const string format, ...) const { + auto level = LogLevel::SUCCESS; + log_at_level_wt + } + + void emit_warning(const string format, ...) const { + auto level = LogLevel::WARNING; + log_at_level + } + + void emit_warning(LoggingTimer *timer, const string format, ...) const { + auto level = LogLevel::WARNING; + log_at_level_it + } + + LoggingTimer emit_warning_wt(const string format, ...) const { + auto level = LogLevel::WARNING; + log_at_level_wt + } + + void emit_error(const string format, ...) const { + auto level = LogLevel::ERROR; + log_at_level + } + + void emit_error(LoggingTimer *timer, const string format, ...) const { + auto level = LogLevel::ERROR; + log_at_level_it + } + + LoggingTimer emit_error_wt(const string format, ...) const { + auto level = LogLevel::ERROR; + log_at_level_wt + } + + virtual void on_stdout_line(string line) const = 0; + + virtual void on_stderr_line(string line) const = 0; + + virtual void on_stdout_line(string prefix, string line) const = 0; + + virtual void on_stderr_line(string prefix, string line) const = 0; + + [[nodiscard]] virtual FILE *get_stdout() const = 0; + + [[nodiscard]] virtual FILE *get_stderr() const = 0; + + [[nodiscard]] virtual LogLevel get_level() const = 0; + }; +} +#endif + +#pragma clang diagnostic pop \ No newline at end of file diff --git a/libkafe/include/kafe/project.hpp b/libkafe/include/kafe/project.hpp new file mode 100644 index 0000000..64bbafa --- /dev/null +++ b/libkafe/include/kafe/project.hpp @@ -0,0 +1,54 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_PROJECT_HPP +#define LIBKAFE_PROJECT_HPP + +#include +#include +#include "kafe/execution_scope.hpp" +#include "kafe/runtime/runtime_exception.hpp" + +using namespace std; +using namespace kafe; +using namespace kafe::runtime; + +namespace kafe { + class ProjectFileException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class UnknownTaskException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class Project { + const string project_file; + public: + explicit Project(string project_file); + + virtual ~Project(); + + void execute( + const Context &context, + const Inventory &inventory + ); + }; +} +#endif diff --git a/libkafe/include/kafe/project/inventory.hpp b/libkafe/include/kafe/project/inventory.hpp new file mode 100644 index 0000000..de5a3da --- /dev/null +++ b/libkafe/include/kafe/project/inventory.hpp @@ -0,0 +1,86 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_PROJECT_INVENTORY_HPP +#define LIBKAFE_PROJECT_INVENTORY_HPP + +#include +#include +#include +#include "kafe/runtime/runtime_exception.hpp" + +using namespace std; +using namespace kafe; +using namespace kafe::runtime; + +namespace kafe::project { + class DuplicateInventoryException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class InventoryItem { + const string user; + const string host; + unsigned int port; + const string environment; + const string role; + + public: + InventoryItem( + string user, + string host, + unsigned int port, + string environment, + string role + ); + + [[nodiscard]] const string &get_environment() const; + + [[nodiscard]] const string &get_role() const; + + [[nodiscard]] const string &get_user() const; + + [[nodiscard]] const string &get_host() const; + + [[nodiscard]] unsigned int get_port() const; + + [[nodiscard]] bool same_as(const InventoryItem &inventoryItem) const; + + [[nodiscard]] string to_string() const; + + [[nodiscard]] string remote_id() const; + }; + + class Inventory { + list items; + + public: + virtual ~Inventory(); + + [[nodiscard]] bool item_exists(const InventoryItem &item) const; + + void add(const InventoryItem &item); + + [[nodiscard]] list find_for_scope( + const string &environment, + const string &role + ) const; + }; +} +#endif diff --git a/libkafe/include/kafe/project/tasks.hpp b/libkafe/include/kafe/project/tasks.hpp new file mode 100644 index 0000000..c273940 --- /dev/null +++ b/libkafe/include/kafe/project/tasks.hpp @@ -0,0 +1,67 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_PROJECT_TASK_HPP +#define LIBKAFE_PROJECT_TASK_HPP + +#include +#include +#include "kafe/runtime/runtime_exception.hpp" + +using namespace std; +using namespace kafe::runtime; + +namespace kafe::project { + class DuplicateTaskException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class UnknownTaskException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class Task { + const string name; + const int function_reference; + const string source; + + public: + Task(string name, int function_reference, string source); + + [[nodiscard]] const string &get_name() const; + + [[nodiscard]] int get_function_reference() const; + + [[nodiscard]] const string &get_source() const; + }; + + class TaskList { + map tasks; + + public: + void add_task(const Task &task); + + [[nodiscard]] bool task_exists(const string &name) const; + + [[nodiscard]] const Task *get_task(const string &name) const; + + virtual ~TaskList(); + }; +} +#endif diff --git a/libkafe/include/kafe/remote/ssh_api.hpp b/libkafe/include/kafe/remote/ssh_api.hpp new file mode 100644 index 0000000..543f73a --- /dev/null +++ b/libkafe/include/kafe/remote/ssh_api.hpp @@ -0,0 +1,65 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_REMOTE_SSH_API_HPP +#define LIBKAFE_REMOTE_SSH_API_HPP + +#include "kafe/logging.hpp" +#include "kafe/remote/ssh_manager.hpp" +#include "kafe/remote/ssh_session.hpp" + +namespace kafe::remote { + class RemoteResult { + string out; + string err; + int code; + + public: + RemoteResult(string &out, string &err, int code); + + [[nodiscard]] const string &get_stdout() const; + + [[nodiscard]] const string &get_stderr() const; + + [[nodiscard]] int get_code() const; + }; + + class SshApi { + SshManager *manager; + const ILogEventListener *log_listener; + string current_chdir; + + public: + SshApi(const SshManager *manager, const ILogEventListener *listener); + + void chdir(const string &string); + + [[nodiscard]] RemoteResult execute(const string &command, bool print_output) const; + + void scp_upload_file(const string &file, const string &remote_path) const; + + void scp_download_file(const string &file, const string &remote_path) const; + + string scp_download_file_as_string(const string &remote_file) const; + + void scp_upload_file_from_string(const string &content, const string &remote_file) const; + }; +} + +#endif diff --git a/libkafe/include/kafe/remote/ssh_manager.hpp b/libkafe/include/kafe/remote/ssh_manager.hpp new file mode 100644 index 0000000..f808a64 --- /dev/null +++ b/libkafe/include/kafe/remote/ssh_manager.hpp @@ -0,0 +1,43 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_REMOTE_SSH_MANAGER_HPP +#define LIBKAFE_REMOTE_SSH_MANAGER_HPP + +#include "kafe/project/inventory.hpp" +#include "kafe/remote/ssh_pool.hpp" +#include "kafe/remote/ssh_session.hpp" +#include "kafe/logging.hpp" + +using namespace kafe; +using namespace kafe::remote; +using namespace kafe::project; + +namespace kafe::remote { + class SshManager { + SshPool *pool; + const InventoryItem *item; + public: + SshManager(const SshPool *pool, const InventoryItem *item); + + const SshSession *get_or_create_session(LogLevel level); + }; +} + +#endif diff --git a/libkafe/include/kafe/remote/ssh_pool.hpp b/libkafe/include/kafe/remote/ssh_pool.hpp new file mode 100644 index 0000000..c4bf87b --- /dev/null +++ b/libkafe/include/kafe/remote/ssh_pool.hpp @@ -0,0 +1,47 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_REMOTE_SSH_POOL_HPP +#define LIBKAFE_REMOTE_SSH_POOL_HPP + +#include +#include +#include "kafe/remote/ssh_session.hpp" + +using namespace std; + +namespace kafe::remote { + // TODO: expire sessions? + class SshPool { + map sessions = {}; + + public: + virtual ~SshPool(); + + [[nodiscard]] bool has_session(const string &remote_id) const; + + [[nodiscard]] SshSession *get_session(const string &remote_id) const; + + void remove_session(const string &remote_id); + + void add_session(const string &remote_id, const SshSession *session); + }; +} + +#endif diff --git a/libkafe/include/kafe/remote/ssh_session.hpp b/libkafe/include/kafe/remote/ssh_session.hpp new file mode 100644 index 0000000..9aed816 --- /dev/null +++ b/libkafe/include/kafe/remote/ssh_session.hpp @@ -0,0 +1,58 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_REMOTE_SSH_SESSION_HPP +#define LIBKAFE_REMOTE_SSH_SESSION_HPP + +#ifndef _LIBSSH_H +extern "C" { +#include "libssh/libssh.h" +#include "libssh/sftp.h" +} +#endif + +#include +#include "kafe/runtime/runtime_exception.hpp" +#include "kafe/logging.hpp" + +using namespace std; +using namespace kafe; +using namespace kafe::runtime; + +namespace kafe::remote { + class SshSessionException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class SshSession { + ssh_session session; + public: + SshSession(const string &user, const string &host, int port, LogLevel level); + + [[nodiscard]] bool is_active() const; + + [[nodiscard]] ssh_session get_ssh_session() const; + + virtual ~SshSession(); + + void close() const; + }; +} + +#endif diff --git a/libkafe/include/kafe/runtime/runtime_exception.hpp b/libkafe/include/kafe/runtime/runtime_exception.hpp new file mode 100644 index 0000000..f72d71e --- /dev/null +++ b/libkafe/include/kafe/runtime/runtime_exception.hpp @@ -0,0 +1,43 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_RUNTIME_RUNTIME_EXCEPTION_HPP +#define LIBKAFE_RUNTIME_RUNTIME_EXCEPTION_HPP + +#include +#include + +using namespace std; + +namespace kafe::runtime { + class RuntimeException : public exception { + string message; + + public: + RuntimeException(); + + explicit RuntimeException(const char *format, ...); + + explicit RuntimeException(const string *format, ...); + + [[nodiscard]] const char *what() const noexcept override; + }; +} + +#endif \ No newline at end of file diff --git a/libkafe/include/kafe/scripting/luainc.hpp b/libkafe/include/kafe/scripting/luainc.hpp new file mode 100644 index 0000000..67889e9 --- /dev/null +++ b/libkafe/include/kafe/scripting/luainc.hpp @@ -0,0 +1,64 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_SCRIPTING_LUAINC_HPP +#define LIBKAFE_SCRIPTING_LUAINC_HPP +extern "C" { +#ifdef KAFE_WITH_LUA_54 +#ifdef KAFE_WITH_LUA_54_VERSIONED +extern "C" { + #include "lua5.4/lua.h" + #include "lua5.4/lualib.h" + #include "lua5.4/lauxlib.h" +} +#elif defined(KAFE_WITH_LUA_ROOT) +extern "C" { + #include "lua.h" + #include "lualib.h" + #include "lauxlib.h" +} +#else +#error "Unable to resolve Lua header file to use" +#endif +#endif + +#ifdef KAFE_WITH_LUA_53 +#ifdef KAFE_WITH_LUA_53_VERSIONED +extern "C" { + #include "lua5.3/lua.h" + #include "lua5.3/lualib.h" + #include "lua5.3/lauxlib.h" +} + +#elif defined(KAFE_WITH_LUA_ROOT) +extern "C" { + #include "lua.h" + #include "lualib.h" + #include "lauxlib.h" +} +#else +#error "Unable to resolve Lua header file to use" +#endif +#endif + +#if not defined(KAFE_WITH_LUA_51) && not defined(KAFE_WITH_LUA_52) && not defined(KAFE_WITH_LUA_53) && not defined(KAFE_WITH_LUA_54) +#error "No suitable Lua found" +#endif +} +#endif diff --git a/libkafe/include/kafe/scripting/script.hpp b/libkafe/include/kafe/scripting/script.hpp new file mode 100644 index 0000000..45599b2 --- /dev/null +++ b/libkafe/include/kafe/scripting/script.hpp @@ -0,0 +1,65 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_SCRIPTING_SCRIPT_HPP +#define LIBKAFE_SCRIPTING_SCRIPT_HPP +#define LIBKAFE_LUA_MODULE_NAME "kafe" + +#include +#include "kafe/scripting/luainc.hpp" +#include "kafe/runtime/runtime_exception.hpp" + +using namespace std; +using namespace kafe; +using namespace kafe::runtime; + +namespace kafe::scripting { + class ScriptEngineException : public RuntimeException { + using RuntimeException::RuntimeException; + }; + + class ScriptEvaluationException : public ScriptEngineException { + using ScriptEngineException::ScriptEngineException; + }; + + class ScriptInvocationException : public ScriptEngineException { + using ScriptEngineException::ScriptEngineException; + }; + + class Script { + const ExecutionScope &scope; + lua_State *lua_state; + + public: + explicit Script(const ExecutionScope &scope); + + virtual ~Script(); + + void load_file(const string &script_file); + + void evaluate(); + + void invoke_function_by_ref(int reference); + + private: + void initialize(); + }; +} + +#endif diff --git a/libkafe/include/kafe/version.hpp b/libkafe/include/kafe/version.hpp new file mode 100644 index 0000000..be46bdf --- /dev/null +++ b/libkafe/include/kafe/version.hpp @@ -0,0 +1,65 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIBKAFE_VERSION_HPP +#define LIBKAFE_VERSION_HPP + +#ifndef LIBKAFE_SCRIPTING_LUAINC_HPP + +#include "kafe/scripting/luainc.hpp" + +#endif +extern "C" { +#ifndef ARCHIVE_H_INCLUDED +#include +#endif +#ifndef __CURL_CURLVER_H +#include +#endif +#ifndef INCLUDE_git_version_h__ +#include +#endif +#ifndef _LIBSSH_H +#include +#endif +} + +#define LIBKAFE_VERSION KAFE_CMAKE_LIB_VERSION +#define LIBKAFE_BUILD_TS KAFE_CMAKE_LIB_BUILD_TS +#define LIBKAFE_VERSION_INT KAFE_CMAKE_LIB_VERSION_INT +#define LIBKAFE_API_LEVEL KAFE_CMAKE_LIB_API_VERSION +#define LIBKAFE_VERSION_MAJOR KAFE_CMAKE_LIB_VERSION_MAJOR +#define LIBKAFE_VERSION_MINOR KAFE_CMAKE_LIB_VERSION_MINOR +#define LIBKAFE_VERSION_RELEASE KAFE_CMAKE_LIB_VERSION_RELEASE + +// Vendor versions +#ifdef ARCHIVE_VERSION_ONLY_STRING +#define LIBKAFE_VERSION_LIB_ARCHIVE ARCHIVE_VERSION_ONLY_STRING +#elif defined(ARCHIVE_VERSION_STRING) +#define LIBKAFE_VERSION_LIB_ARCHIVE ARCHIVE_VERSION_STRING +#else +#define LIBKAFE_VERSION_LIB_ARCHIVE "???" +#endif + +#define LIBKAFE_VERSION_LIB_LUA LUA_VERSION +#define LIBKAFE_VERSION_LIB_SSH SSH_STRINGIFY(LIBSSH_VERSION) +#define LIBKAFE_VERSION_LIB_CURL LIBCURL_VERSION +#define LIBKAFE_VERSION_LIB_GIT2 LIBGIT2_VERSION + +#endif \ No newline at end of file diff --git a/libkafe/src/context.cpp b/libkafe/src/context.cpp new file mode 100644 index 0000000..9b2359f --- /dev/null +++ b/libkafe/src/context.cpp @@ -0,0 +1,49 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "kafe/context.hpp" + +namespace kafe { + Context::Context( + const map &envvals, + string environment, + vector tasks, + const ILogEventListener *log_listener + ) : envvals(envvals), environment(move(environment)), tasks(move(tasks)), log_listener(log_listener) { + } + + Context::~Context() = default; + + const string &Context::get_environment() const { + return environment; + } + + const vector &Context::get_tasks() const { + return tasks; + } + + const ILogEventListener *Context::get_log_listener() const { + return log_listener; + } + + const map &Context::get_envvals() const { + return envvals; + } +} \ No newline at end of file diff --git a/libkafe/src/execution_scope.cpp b/libkafe/src/execution_scope.cpp new file mode 100644 index 0000000..c44b4b1 --- /dev/null +++ b/libkafe/src/execution_scope.cpp @@ -0,0 +1,160 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "kafe/execution_scope.hpp" +#include "kafe/io/file_system.hpp" + +using namespace kafe::io; +using namespace kafe::local; +using namespace kafe::remote; + +namespace kafe { + ExecutionScope::ExecutionScope( + const Context &context, + const Inventory &inventory + ) : context(context), inventory(inventory) { + this->ssh_pool = new SshPool(); + this->tasks = new TaskList(); + this->local = new LocalApi(context.get_log_listener()); + } + + const Context *ExecutionScope::get_context() const { + return &context; + } + + const Inventory *ExecutionScope::get_inventory() const { + return &inventory; + } + + const TaskList *ExecutionScope::get_tasks() const { + return tasks; + } + + const SshPool *ExecutionScope::get_ssh_pool() const { + return ssh_pool; + } + + void ExecutionScope::set_current_remote(SshApi *api) { + this->current_api = api; + } + + const SshApi *ExecutionScope::get_current_api() const { + return this->current_api; + } + + bool ExecutionScope::has_current_api() const { + return nullptr != this->current_api; + } + + void ExecutionScope::clear_current_api() { + this->current_api = nullptr; + } + + void ExecutionScope::add_rm_on_destruct(const string &file) { + files_to_rm_on_destruct.push_back(file); + } + + ExecutionScope::~ExecutionScope() { + clear_current_api(); + + for (const auto &file: files_to_rm_on_destruct) { + try { + FileSystem::try_rm_r(file); + } catch (exception &e) { + // NOOP + } + } + + delete this->local; + } + + void ExecutionScope::define_var(const string &key, const string &value) { + values[key] = value; + } + + const map &ExecutionScope::get_vars() const { + return values; + } + + static const char *const START_TOK = "{{"; + static const size_t STRLEN_START_TOK = strlen(START_TOK); + + static const char *const END_TOK = "}}"; + static const size_t STRLEN_END_TOK = strlen(END_TOK); + + string ExecutionScope::replace_vars(const string &input) const { + ostringstream out; + size_t pos = 0; + for (;;) { + size_t subst_pos = input.find(START_TOK, pos); + size_t end_pos = input.find(END_TOK, subst_pos); + + if (end_pos == string::npos) { + break; + } + + out.write(&*input.begin() + pos, subst_pos - pos); + subst_pos += STRLEN_START_TOK; + auto subst_it = values.find(input.substr(subst_pos, end_pos - subst_pos)); + + if (subst_it != values.end()) { + out << subst_it->second; + } + + pos = end_pos + STRLEN_END_TOK; + } + out << input.substr(pos, string::npos); + + return out.str(); + } + + string ExecutionScope::replace_env(const string &input) const { + auto envvals = context.get_envvals(); + ostringstream out; + size_t pos = 0; + for (;;) { + size_t subst_pos = input.find(START_TOK, pos); + size_t end_pos = input.find(END_TOK, subst_pos); + + if (end_pos == string::npos) { + break; + } + + out.write(&*input.begin() + pos, subst_pos - pos); + subst_pos += STRLEN_START_TOK; + const char *key = input.substr(subst_pos, end_pos - subst_pos).c_str(); + auto env_val = envvals.find(key); + + if (env_val != envvals.end()) { + out << env_val->second; + } + + pos = end_pos + STRLEN_END_TOK; + } + out << input.substr(pos, string::npos); + + return out.str(); + } + + LocalApi *ExecutionScope::get_local_api() const { + return local; + } +} \ No newline at end of file diff --git a/libkafe/src/io/archive.cpp b/libkafe/src/io/archive.cpp new file mode 100644 index 0000000..b6908eb --- /dev/null +++ b/libkafe/src/io/archive.cpp @@ -0,0 +1,116 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "kafe/io/archive.hpp" +#include "kafe/io/file_system.hpp" +#include "kafe/runtime/runtime_exception.hpp" + +using namespace std; +using namespace kafe; +using namespace kafe::runtime; + +namespace kafe::io { + static const int ARCHIVE_FILE_BUFFER_S = 4096; + + string Archive::tmp_archive_from_directory(const string &directory) { + auto name = tmpnam(nullptr); // TODO replace + auto upload_name = string(name) + ".tar.gz"; + archive_from_directory(upload_name, directory); + return upload_name; + } + + // TODO: implement .kafeignore + // TODO: https://stackoverflow.com/questions/23330065/adding-directory-to-tarfile-with-libarchive + void Archive::archive_from_directory(const string &archive_path, const string &directory) { + if (!FileSystem::is_directory(directory)) { + throw RuntimeException("Can not create archive - directory <%s> not found", directory.c_str()); + } + + if (FileSystem::is_file_or_symlink(archive_path)) { + throw RuntimeException("Can not create archive - path <%s> exists", directory.c_str()); + } + + auto archive_dir_name = std_fs::path(archive_path).parent_path(); + + if (FileSystem::exists(archive_dir_name)) { + if (!FileSystem::is_directory(archive_dir_name)) { + throw RuntimeException("Can not create archive - output directory <%s> not found", + archive_dir_name.c_str()); + } + } else { + FileSystem::mkdirs(archive_dir_name); + } + + std_fs::recursive_directory_iterator iter(directory), end; + + auto *archive = archive_write_new(); + archive_write_add_filter_gzip(archive); + archive_write_set_format_pax_restricted(archive); + archive_write_open_filename(archive, archive_path.c_str()); + + while (iter != end) { + auto path_abs = std_fs::absolute(iter->path()); + + if (std_fs::is_directory(path_abs)) { + ++iter; + continue; + } + + // TODO: this is not nice at all.. + auto path_rel = iter->path().string().substr(directory.size()); + + if (path_rel[0] == '/') { + path_rel = path_rel.substr(1, path_rel.size() - 1); + } + // ENDTODO + + auto size = std_fs::file_size(path_abs); + + struct stat buf{}; + stat(path_abs.c_str(), &buf); + + auto *entry = archive_entry_new(); + archive_entry_set_pathname(entry, path_rel.c_str()); + archive_entry_set_size(entry, size); + archive_entry_set_filetype(entry, AE_IFREG); + archive_entry_set_perm(entry, buf.st_mode); +// archive_entry_set_mtime(entry, buf.st_mtim.tv_sec, 0); + archive_write_header(archive, entry); + archive_entry_copy_stat(entry, &buf); + + ifstream fin(path_abs.string(), ifstream::binary); + char buffer[ARCHIVE_FILE_BUFFER_S]; + do { + fin.read(buffer, ARCHIVE_FILE_BUFFER_S); + archive_write_data(archive, buffer, fin.gcount()); + } while (fin); + + fin.close(); + archive_entry_free(entry); + + ++iter; + } + + archive_write_close(archive); + archive_write_free(archive); + } +} diff --git a/libkafe/src/io/file_system.cpp b/libkafe/src/io/file_system.cpp new file mode 100644 index 0000000..80cee2b --- /dev/null +++ b/libkafe/src/io/file_system.cpp @@ -0,0 +1,144 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kafe/io/file_system.hpp" +#include "kafe/runtime/runtime_exception.hpp" + +using namespace kafe::runtime; +using namespace kafe::io; + +namespace kafe::io { + bool FileSystem::is_file_or_symlink(const string &path_s) { + return (std_fs::is_regular_file(path_s) || std_fs::is_symlink(path_s)) + && !std_fs::is_block_file(path_s); + } + + bool FileSystem::is_directory(const string &path_s) { + return std_fs::is_directory(std_fs::absolute(path_s)); + } + + bool FileSystem::exists(const string &path) { + return std_fs::exists(path); + } + + bool FileSystem::mkdirs(const string &path) { + return std_fs::create_directories(path); + } + + void FileSystem::try_rm_r(const string &path) { + if (path.empty()) { + return; + } + + if (!exists(path)) { + return; + } + + auto fs_path_abs = std_fs::absolute(path); + + if (std_fs::is_symlink(fs_path_abs)) { + fs_path_abs = std_fs::absolute(std_fs::read_symlink(fs_path_abs)); + } + + if ("/" == fs_path_abs) { + // TODO warn about the dire consequences of stupidity... + return; + } + + if (is_directory(fs_path_abs)) { + std_fs::remove_all(fs_path_abs); + return; + } + + std_fs::remove(fs_path_abs); + } + + /** + * This method has been copied from Boost filesystem, version 1.65.0 + */ + std_fs::path FileSystem::absolute(const std_fs::path &p, const std_fs::path &base) { + // recursively calling absolute is sub-optimal, but is sure and simple + std_fs::path abs_base(base.is_absolute() ? base : FileSystem::absolute(base, std_fs::current_path())); + + // store expensive to compute values that are needed multiple times + std_fs::path p_root_name(p.root_name()); + std_fs::path base_root_name(abs_base.root_name()); + std_fs::path p_root_directory(p.root_directory()); + + if (p.empty()) + return abs_base; + + if (!p_root_name.empty()) // p.has_root_name() + { + if (p_root_directory.empty()) // !p.has_root_directory() + return p_root_name / abs_base.root_directory() + / abs_base.relative_path() / p.relative_path(); + // p is absolute, so fall through to return p at end of block + } else if (!p_root_directory.empty()) // p.has_root_directory() + { +# ifdef BOOST_POSIX_API + // POSIX can have root name it it is a network path + if (base_root_name.empty()) // !abs_base.has_root_name() + return p; +# endif + return base_root_name / p; + } else { + return abs_base / p; + } + + return p; // p.is_absolute() is true + } + + std_fs::path FileSystem::expand(const std_fs::path &p) { + if (p.empty()) { return p; } + // TODO: how does this affect env of the script? perhaps need an arg with home dir? +#ifdef _GNU_SOURCE + const char *home = secure_getenv("HOME"); +#else + const char *home = getenv("HOME"); +#endif + if (home == nullptr) { + throw RuntimeException("HOME environment variable not set - can not expand home directory"); + } + + string s = p.c_str(); + if ('~' == s[0]) { + s = string(home) + s.substr(1, s.size() - 1); + return std_fs::path(s); + } else { + return p; + } + } + + std_fs::path FileSystem::normalize(const std_fs::path &p, const std_fs::path &base) { + auto base_p = base.empty() ? std_fs::current_path() : base; + + if (p.empty()) { + return absolute(p, base_p); + } + + auto abs_p = absolute(expand(p), base_p); + + try { + return std_fs::canonical(abs_p); + } catch (exception &e) { + return abs_p; + } + } +} \ No newline at end of file diff --git a/libkafe/src/local/local_api.cpp b/libkafe/src/local/local_api.cpp new file mode 100644 index 0000000..fabe1ee --- /dev/null +++ b/libkafe/src/local/local_api.cpp @@ -0,0 +1,139 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "kafe/local/local_api.hpp" +#include "kafe/io/file_system.hpp" +#include "kafe/logging.hpp" + +using namespace std; +using namespace kafe::io; + +namespace kafe::local { + LocalShellResult::LocalShellResult(string out, const int code) : out(move(out)), code(code) {} + + string LocalShellResult::get_out() const { + return out; + } + + int LocalShellResult::get_code() const { + return code; + } + + LocalApi::LocalApi(const ILogEventListener *log_listener) : log_listener(log_listener) { + } + + string LocalApi::read_out(FILE *pFile, bool print_output) { + const size_t buffer_size = 256; + size_t output_size = 4096; + + char *output = (char *) malloc(output_size); + unsigned long current_pos = 0; + unsigned long last_line_pos = 0; + size_t n_read; + char buffer[buffer_size]; + char *line_buffer = (char *) malloc(256); + do { + n_read = fread(buffer, 1, buffer_size, pFile); + if (n_read > 0) { + for (int i = 0; i < n_read; i++) { + auto c = buffer[i]; + output[current_pos] = c; + + if (print_output && c == '\n') { + const unsigned long size_of = sizeof(char) * (current_pos - last_line_pos + 1); + line_buffer = (char *) realloc(line_buffer, size_of + 1); + memcpy(line_buffer, output + last_line_pos, size_of); + line_buffer[size_of] = '\0'; + + log_listener->on_stdout_line("-> out", line_buffer); + + last_line_pos = current_pos + 1; + } + + current_pos++; + + if (current_pos > output_size) { + output_size += buffer_size; + output = (char *) realloc(output, output_size); + } + } + } + } while (n_read > 0); + + // Strip last line + if (output[current_pos - 1] == '\n') { + output[current_pos - 1] = '\0'; + } else { + output[current_pos] = '\0'; + } + + auto result = string(output); + + free(line_buffer); + free(output); + + return result; + } + + // TODO: this works, but does not capture stderr for obvious reasons... + LocalShellResult LocalApi::local_popen(const string &command, bool print_output) { + string cmd; + LoggingTimer timer; + + if (!this->current_chdir.empty()) { + timer = log_listener->emit_info_wt( + "In directory <%s> executing <%s>", this->current_chdir.c_str(), command.c_str()); + cmd = "cd " + this->current_chdir + " && " + string(command); + } else { + timer = log_listener->emit_info_wt("Executing local command %s", command.c_str()); + cmd = string(command); + } + + log_listener->emit_debug("Full local shell command is <%s>", cmd.c_str()); + + FILE *p = ::popen(cmd.c_str(), "r"); + + if (nullptr == p) { + return LocalShellResult("", -1); + } + + auto output = read_out(p, print_output); + int exit_code = pclose(p); + + if (0 == exit_code) { + log_listener->emit_info(&timer, "Local command complete"); + } else { + log_listener->emit_warning(&timer, "Local command complete with non-zero exit code <%d>", exit_code); + } + + return LocalShellResult(output, exit_code); + } + + void LocalApi::chdir(const string &chdir) { + this->current_chdir = FileSystem::normalize(chdir, std_fs::current_path()); + } + + const string &LocalApi::get_chdir() const { + return current_chdir; + } +} \ No newline at end of file diff --git a/libkafe/src/project.cpp b/libkafe/src/project.cpp new file mode 100644 index 0000000..fc6138c --- /dev/null +++ b/libkafe/src/project.cpp @@ -0,0 +1,78 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "kafe/project.hpp" +#include "kafe/execution_scope.hpp" +#include "kafe/io/file_system.hpp" +#include "kafe/scripting/script.hpp" + +using namespace std; +using namespace kafe::io; +using namespace kafe::scripting; + +namespace kafe { + Project::Project(string project_file) : project_file(move(project_file)) { + if (!FileSystem::is_file_or_symlink(this->project_file)) { + throw ProjectFileException("Project file <%s> does not exist", this->project_file.c_str()); + } + } + + Project::~Project() = default; + + void Project::execute( + const Context &context, + const Inventory &inventory + ) { + auto scope = ExecutionScope(context, inventory); + LoggingTimer timer; + + auto script = Script(scope); + auto *logger = const_cast(scope.get_context()->get_log_listener()); + + timer = logger->emit_info_wt("Loading project file <%s>", project_file.c_str()); + script.load_file(project_file); + logger->emit_success(&timer, "Loaded project file <%s>", project_file.c_str()); + + timer = logger->emit_info_wt("Evaluating project file <%s>", project_file.c_str()); + script.evaluate(); + logger->emit_success(&timer, "Done evaluating project file <%s>", project_file.c_str()); + + logger->emit_info("Verifying all tasks requested are defined"); + for (auto &task_name : scope.get_context()->get_tasks()) { + if (!scope.get_tasks()->task_exists(task_name)) { + throw UnknownTaskException("Task <%s> is not defined in project", task_name.c_str()); + } + } + logger->emit_success("Tasks verified"); + + for (auto &task_name : scope.get_context()->get_tasks()) { + timer = logger->emit_info_wt("Executing task <%s>", task_name.c_str()); + logger->context_push(task_name); + + const Task *task = scope.get_tasks()->get_task(task_name); + int ref = task->get_function_reference(); + + script.invoke_function_by_ref(ref); + + logger->context_pop(); + logger->emit_success(&timer, "Task <%s> completed", task_name.c_str()); + } + } +} \ No newline at end of file diff --git a/libkafe/src/project/inventory.cpp b/libkafe/src/project/inventory.cpp new file mode 100644 index 0000000..7250ff9 --- /dev/null +++ b/libkafe/src/project/inventory.cpp @@ -0,0 +1,113 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kafe/project/inventory.hpp" +#include + +namespace kafe::project { + InventoryItem::InventoryItem( + string user, + string host, + unsigned int port, + string environment, + string role + ) : environment(move(environment)), role(move(role)), user(move(user)), host(move(host)), port(port) {} + + const string &InventoryItem::get_environment() const { + return environment; + } + + const string &InventoryItem::get_role() const { + return role; + } + + const string &InventoryItem::get_user() const { + return user; + } + + const string &InventoryItem::get_host() const { + return host; + } + + unsigned int InventoryItem::get_port() const { + return port; + } + + bool InventoryItem::same_as(const InventoryItem &other) const { + return other.get_user() == user + && other.get_host() == host + && other.get_port() == port + && other.get_environment() == environment + && other.get_role() == role; + } + + string InventoryItem::to_string() const { + return get_environment() + "+" + role + "://" + + get_user() + "@" + get_host() + ":" + ::to_string(get_port()); + } + + string InventoryItem::remote_id() const { + return get_user() + "@" + get_host() + ":" + ::to_string(get_port()); + } + + bool Inventory::item_exists(const InventoryItem &item) const { + for (const InventoryItem *local : this->items) { + if (local->same_as(item)) { + return true; + } + } + return false; + } + + void Inventory::add(const InventoryItem &item) { + if (item_exists(item)) { + throw DuplicateInventoryException("Duplicate inventory item <%s>", item.to_string().c_str()); + } + + this->items.push_back(&item); + } + + list Inventory::find_for_scope(const string &environment, const string &role) const { + list scoped; + + for (const InventoryItem *item : this->items) { + if (item->get_environment() != environment) { + continue; + } + + const auto &item_role = item->get_role(); + + if (item_role != role) { + continue; + } + + scoped.push_back(item); + } + + return scoped; + } + + Inventory::~Inventory() { + for (auto item : items) { + delete (item); + } + items.clear(); + } +} + diff --git a/libkafe/src/project/tasks.cpp b/libkafe/src/project/tasks.cpp new file mode 100644 index 0000000..5beca8b --- /dev/null +++ b/libkafe/src/project/tasks.cpp @@ -0,0 +1,75 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kafe/project/tasks.hpp" + +#include + +using namespace std; + +namespace kafe::project { + Task::Task(string name, int function_reference, string source) + : name(move(name)), function_reference(function_reference), source(move(source)) { + } + + const string &Task::get_name() const { + return name; + } + + int Task::get_function_reference() const { + return function_reference; + } + + const string &Task::get_source() const { + return source; + } + + void TaskList::add_task(const Task &task) { + const auto &name = task.get_name(); + + if (task_exists(name)) { + throw DuplicateTaskException("Duplicate task <%s>", name.c_str()); + } + + pair entry = pair(name, &task); + this->tasks.insert(entry); + } + + const Task *TaskList::get_task(const string &name) const { + auto task = tasks.find(name); + + if (task == tasks.end()) { + throw UnknownTaskException("Unknown task <%s>", name.c_str()); + } + + return task->second; + } + + bool TaskList::task_exists(const string &name) const { + return tasks.find(name) != tasks.end(); + } + + TaskList::~TaskList() { + for (const auto &task : tasks) { + delete (task.second); + } + tasks.clear(); + } +} + diff --git a/libkafe/src/remote/ssh_api.cpp b/libkafe/src/remote/ssh_api.cpp new file mode 100644 index 0000000..c15fc6d --- /dev/null +++ b/libkafe/src/remote/ssh_api.cpp @@ -0,0 +1,376 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "kafe/remote/ssh_api.hpp" +#include "kafe/io/file_system.hpp" + +using namespace kafe; +using namespace kafe::io; + +namespace kafe::remote { + RemoteResult::RemoteResult(string &out, string &err, int code) : out(out), err(err), code(code) { + } + + const string &RemoteResult::get_stdout() const { + return out; + } + + const string &RemoteResult::get_stderr() const { + return err; + } + + int RemoteResult::get_code() const { + return code; + } + + static string ssh_read_channel_out( + const ILogEventListener *listener, + ssh_channel channel, + int is_stderr, + bool print_output + ) { + const size_t buffer_size = 256; + size_t output_size = 4096; + + char *output = (char *) malloc(output_size); + unsigned long current_pos = 0; + unsigned long last_line_pos = 0; + int ssh_n_read; + char buffer[buffer_size]; + char *line_buffer = (char *) malloc(256); + do { + ssh_n_read = ssh_channel_read_timeout(channel, buffer, buffer_size, is_stderr, 1800000); + if (ssh_n_read > 0) { + for (int i = 0; i < ssh_n_read; i++) { + auto c = buffer[i]; + output[current_pos] = c; + + if (print_output && c == '\n') { + const unsigned long size_of = sizeof(char) * (current_pos - last_line_pos + 1); + line_buffer = (char *) realloc(line_buffer, size_of + 1); + memcpy(line_buffer, output + last_line_pos, size_of); + line_buffer[size_of] = '\0'; + + if (is_stderr) { + listener->on_stderr_line(">> err", line_buffer); + } else { + listener->on_stdout_line(">> out", line_buffer); + } + + last_line_pos = current_pos + 1; + } + + current_pos++; + + if (current_pos > output_size) { + output_size += buffer_size; + output = (char *) realloc(output, output_size); + } + } + } + } while (ssh_n_read > 0); + + // Strip last line + if (output[current_pos - 1] == '\n') { + output[current_pos - 1] = '\0'; + } else { + output[current_pos] = '\0'; + } + + auto result = string(output); + + delete[](line_buffer); + delete[](output); + + return result; + } + + SshApi::SshApi(const SshManager *manager, const ILogEventListener *log_listener) + : manager(const_cast(manager)), log_listener(log_listener) {} + + void SshApi::chdir(const string &chdir) { + this->current_chdir = chdir; + } + + RemoteResult SshApi::execute(const string &command, const bool print_output) const { + auto session = manager->get_or_create_session(log_listener->get_level()); + auto ssh_session = session->get_ssh_session(); + + ssh_channel channel; + int rc; + + channel = ssh_channel_new(ssh_session); + if (nullptr == channel) { + throw RuntimeException("SSH error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + rc = ssh_channel_open_session(channel); + if (SSH_OK != rc) { + ssh_channel_free(channel); + throw RuntimeException("SSH error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + ostringstream cmd_buf; + + LoggingTimer timer; + if (!this->current_chdir.empty()) { + timer = log_listener->emit_info_wt( + "In directory <%s> executing <%s>", this->current_chdir.c_str(), command.c_str()); + cmd_buf << "cd " << this->current_chdir << " && "; + } else { + timer = log_listener->emit_info_wt("Executing command %s", command.c_str()); + } + + cmd_buf << command; + + log_listener->emit_debug("Full shell command is <%s>", cmd_buf.str().c_str()); + + rc = ssh_channel_request_exec(channel, cmd_buf.str().c_str()); + + timer.stop(); + + if (SSH_OK != rc) { + ssh_channel_close(channel); + ssh_channel_free(channel); + + throw RuntimeException("SSH error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + auto out = ssh_read_channel_out(log_listener, channel, 0, print_output); + auto err = ssh_read_channel_out(log_listener, channel, 1, print_output); + + if (ssh_channel_is_open(channel)) { + ssh_channel_send_eof(channel); + ssh_channel_close(channel); + } + + auto e = ssh_channel_get_exit_status(channel); + + ssh_channel_free(channel); + + if (0 == e) { + log_listener->emit_info(&timer, "Command complete"); + } else { + log_listener->emit_warning(&timer, "Command complete with non-zero exit code <%d>", e); + } + + return RemoteResult(out, err, e); + } + + void SshApi::scp_upload_file(const string &file, const string &remote_file) const { + if (!FileSystem::is_file_or_symlink(file)) { + throw RuntimeException("File <%s> is not file", file.c_str()); + } + + auto file_path = std_fs::path(file); + auto remote_file_path = std_fs::path(remote_file); + + std_fs::path remote_dir; + if (!current_chdir.empty()) { + remote_dir = FileSystem::absolute(remote_file, current_chdir); + } else { + remote_dir = remote_file_path; + } + + auto session = manager->get_or_create_session(log_listener->get_level()); + auto ssh_session = session->get_ssh_session(); + + auto scp = ssh_scp_new(ssh_session, SSH_SCP_WRITE, remote_dir.c_str()); + + if (nullptr == scp) { + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + if (SSH_OK != ssh_scp_init(scp)) { + ssh_scp_free(scp); + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + auto size = std_fs::file_size(file_path); + + auto rc = ssh_scp_push_file(scp, file_path.filename().c_str(), size, 0400 | 0200); + + if (SSH_OK != rc) { + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + bool failed = false; + ifstream fin(file, ifstream::binary); + char buffer[4096]; + do { + fin.read(buffer, 4096); + auto rcc = ssh_scp_write(scp, buffer, fin.gcount()); + + if (SSH_OK != rcc) { + failed = true; + break; + } + } while (fin); + fin.close(); + + if (failed) { + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + ssh_scp_close(scp); + ssh_scp_free(scp); + } + + void SshApi::scp_upload_file_from_string(const string &content, const string &remote_file) const { + auto remote_file_path = std_fs::path(remote_file); + + std_fs::path remote_dir; + if (!current_chdir.empty()) { + remote_dir = FileSystem::absolute(remote_file, current_chdir); + } else { + remote_dir = remote_file_path; + } + + auto session = manager->get_or_create_session(log_listener->get_level()); + auto ssh_session = session->get_ssh_session(); + + auto scp = ssh_scp_new(ssh_session, SSH_SCP_WRITE, remote_dir.c_str()); + + if (nullptr == scp) { + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + if (SSH_OK != ssh_scp_init(scp)) { + ssh_scp_free(scp); + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + auto rc = ssh_scp_push_file(scp, remote_file.c_str(), content.size(), 0400 | 0200); + + if (SSH_OK != rc) { + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + auto rcc = ssh_scp_write(scp, content.c_str(), content.size()); + + if (SSH_OK != rcc) { + throw RuntimeException("SCP error [code %d: %s]", ssh_get_error_code(ssh_session), + ssh_get_error(ssh_session)); + } + + ssh_scp_close(scp); + ssh_scp_free(scp); + } + + void SshApi::scp_download_file(const string &file, const string &remote_file) const { + auto session = manager->get_or_create_session(log_listener->get_level()); + auto ssh_session = session->get_ssh_session(); + + auto scp = ssh_scp_new(ssh_session, SSH_SCP_READ, remote_file.c_str()); + if (scp == nullptr) { + throw RuntimeException("SSH error: %s", ssh_get_error(ssh_session)); + } + + auto rc = ssh_scp_init(scp); + if (rc != SSH_OK) { + ssh_scp_free(scp); + throw RuntimeException("SSH error: %s", ssh_get_error(ssh_session)); + } + + rc = ssh_scp_pull_request(scp); + if (rc != SSH_SCP_REQUEST_NEWFILE) { + ssh_scp_free(scp); + throw RuntimeException("SSH error: %s", ssh_get_error(ssh_session)); + } + + auto size = ssh_scp_request_get_size(scp); + + ssh_scp_accept_request(scp); + + ofstream out(file, ofstream::binary); + const int bsize = size > 4096 ? 4096 : (int) size; + char *buffer = (char *) malloc(bsize); + int read_count; + do { + read_count = ssh_scp_read(scp, buffer, bsize); + if (read_count > 0) { + out.write(buffer, read_count); + } + } while (read_count > 0); + + out.close(); + free(buffer); + + ssh_scp_close(scp); + ssh_scp_free(scp); + } + + string SshApi::scp_download_file_as_string(const string &remote_file) const { + auto session = manager->get_or_create_session(log_listener->get_level()); + auto ssh_session = session->get_ssh_session(); + + auto scp = ssh_scp_new(ssh_session, SSH_SCP_READ, remote_file.c_str()); + if (scp == nullptr) { + throw RuntimeException("SSH error: %s", ssh_get_error(ssh_session)); + } + + auto rc = ssh_scp_init(scp); + if (rc != SSH_OK) { + ssh_scp_free(scp); + throw RuntimeException("SSH error: %s", ssh_get_error(ssh_session)); + } + + rc = ssh_scp_pull_request(scp); + if (rc != SSH_SCP_REQUEST_NEWFILE) { + ssh_scp_free(scp); + throw RuntimeException("SSH error: %s", ssh_get_error(ssh_session)); + } + + auto size = ssh_scp_request_get_size(scp); + + ssh_scp_accept_request(scp); + + stringstream bfs; + const int bsize = size > 4096 ? 4096 : (int) size; + char *buffer = (char *) malloc(bsize); + int read_count; + do { + read_count = ssh_scp_read(scp, buffer, bsize); + if (read_count > 0) { + bfs.write(buffer, read_count); + } + } while (read_count > 0); + + free(buffer); + + ssh_scp_close(scp); + ssh_scp_free(scp); + + return bfs.str(); + } +} diff --git a/libkafe/src/remote/ssh_manager.cpp b/libkafe/src/remote/ssh_manager.cpp new file mode 100644 index 0000000..f12a954 --- /dev/null +++ b/libkafe/src/remote/ssh_manager.cpp @@ -0,0 +1,51 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kafe/remote/ssh_manager.hpp" + +namespace kafe::remote { + SshManager::SshManager(const SshPool *pool, const InventoryItem *item) + : pool(const_cast(pool)), item(item) { + } + + const SshSession *SshManager::get_or_create_session(LogLevel level) { + auto remote_id = item->remote_id(); + + if (pool->has_session(remote_id)) { + auto current_session = pool->get_session(remote_id); + + if (current_session->is_active()) { + return current_session; + } + + pool->remove_session(remote_id); + } + + auto session = new SshSession( + item->get_user(), + item->get_host(), + item->get_port(), + level + ); + + pool->add_session(remote_id, session); + + return session; + } +} \ No newline at end of file diff --git a/libkafe/src/remote/ssh_pool.cpp b/libkafe/src/remote/ssh_pool.cpp new file mode 100644 index 0000000..f37318d --- /dev/null +++ b/libkafe/src/remote/ssh_pool.cpp @@ -0,0 +1,59 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kafe/remote/ssh_pool.hpp" + +namespace kafe::remote { + SshPool::~SshPool() { + for (const auto&[key, value] : sessions) { + value->close(); + delete (value); + } + + sessions.clear(); + } + + bool SshPool::has_session(const string &remote_id) const { + return this->sessions.find(remote_id) != this->sessions.end(); + } + + SshSession *SshPool::get_session(const string &remote_id) const { + if (!has_session(remote_id)) { + return nullptr; + } + + return const_cast(this->sessions.find(remote_id)->second); + } + + void SshPool::add_session(const string &remote_id, const SshSession *session) { + auto entry = pair(remote_id, session); + this->sessions.insert(entry); + } + + void SshPool::remove_session(const string &remote_id) { + auto candidate = sessions.find(remote_id); + + if (candidate == sessions.end()) { + return; + } + + sessions.erase(remote_id); + delete (candidate->second); + } +} \ No newline at end of file diff --git a/libkafe/src/remote/ssh_session.cpp b/libkafe/src/remote/ssh_session.cpp new file mode 100644 index 0000000..3856e19 --- /dev/null +++ b/libkafe/src/remote/ssh_session.cpp @@ -0,0 +1,81 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kafe/remote/ssh_session.hpp" + +namespace kafe::remote { + SshSession::SshSession(const string &user, const string &host, int port, LogLevel level) { + ssh_session session_new = ssh_new(); + + int verbosity; + if (level == LogLevel::ALL) { + verbosity = SSH_LOG_FUNCTIONS; + } else if (level == LogLevel::TRACE) { + verbosity = SSH_LOG_PROTOCOL; + } else { + verbosity = SSH_LOG_NOLOG; + } + + ssh_options_set(session_new, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); + ssh_options_set(session_new, SSH_OPTIONS_USER, user.c_str()); + ssh_options_set(session_new, SSH_OPTIONS_HOST, host.c_str()); + ssh_options_set(session_new, SSH_OPTIONS_PORT, &port); + + auto result = ssh_connect(session_new); + + if (SSH_OK != result) { + throw SshSessionException(); + } + + auto is_known = ssh_is_server_known(session_new); + if (is_known != SSH_SERVER_KNOWN_OK) { + throw SshSessionException(); + } + + // TODO: load passphrase from env properties + auto is_auth = ssh_userauth_publickey_auto(session_new, nullptr, nullptr); + + // TODO: verify + if (is_auth != SSH_AUTH_SUCCESS) { + throw SshSessionException(); + } + + this->session = session_new; + } + + SshSession::~SshSession() { + if (is_active()) { + ssh_free(this->session); + } + } + + bool SshSession::is_active() const { + return static_cast(ssh_is_connected(session)); + } + + ssh_session SshSession::get_ssh_session() const { + return session; + } + + void SshSession::close() const { + if (ssh_is_connected(session)) { + ssh_disconnect(session); + } + } +} \ No newline at end of file diff --git a/libkafe/src/runtime/runtime_exception.cpp b/libkafe/src/runtime/runtime_exception.cpp new file mode 100644 index 0000000..60e3b4c --- /dev/null +++ b/libkafe/src/runtime/runtime_exception.cpp @@ -0,0 +1,50 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "kafe/runtime/runtime_exception.hpp" + +namespace kafe::runtime { + RuntimeException::RuntimeException() { + message = exception::what(); + } + + RuntimeException::RuntimeException(const char *format, ...) { + char buffer[4096]; + va_list args; + va_start(args, format); + vsnprintf(buffer, 4096, format, args); + va_end(args); + this->message = buffer; + } + + RuntimeException::RuntimeException(const string *format, ...) { + char buffer[4096]; + va_list args; + va_start(args, format); + vsnprintf(buffer, 4096, format->c_str(), args); + va_end(args); + this->message = buffer; + } + + const char *RuntimeException::what() const noexcept { + return message.c_str(); + } +} \ No newline at end of file diff --git a/libkafe/src/scripting/script.cpp b/libkafe/src/scripting/script.cpp new file mode 100644 index 0000000..70fc9e3 --- /dev/null +++ b/libkafe/src/scripting/script.cpp @@ -0,0 +1,932 @@ +/** + * This file is part of Kafe. + * https://github.com/libkafe/kafe/ + * + * Copyright 2020 Matiss Treinis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "kafe/version.hpp" +#include "kafe/execution_scope.hpp" +#include "kafe/scripting/script.hpp" +#include "kafe/remote/ssh_manager.hpp" +#include "kafe/remote/ssh_api.hpp" +#include "kafe/io/archive.hpp" +#include "kafe/io/file_system.hpp" + +using namespace kafe::io; + +static mutex state_lock; +static map state; + +static inline ExecutionScope *get_scope(lua_State *L) { + auto res = state.find(L); + + if (res == state.end()) { + throw RuntimeException("Out of scope"); + } + + return const_cast(res->second); +} + +// TODO - review error messages +namespace kafe::scripting { + + // Lua stdout/stderr + // TODO: split line by line + static int lua_logger_print(lua_State *L, bool is_err) { + auto scope = get_scope(L); + int n_args = lua_gettop(L); + lua_getglobal(L, "tostring"); + for (int i = 1; i <= n_args; i++) { + lua_pushvalue(L, -1); + lua_pushvalue(L, i); + lua_call(L, 1, 1); + auto line = lua_tostring(L, -1); + if (nullptr == line) { + return luaL_error(L, "'tostring' must return a string to 'print'"); + } + + auto with_vars = scope->replace_vars(line); + + if (is_err) { + scope->get_context()->get_log_listener()->on_stderr_line("#! err", with_vars); + } else { + scope->get_context()->get_log_listener()->on_stdout_line("#! out", with_vars); + } + lua_pop(L, 1); + } + return 0; + } + + static int lua_logger_print(lua_State *L) { + return lua_logger_print(L, false); + } + + static int lua_logger_print_err(lua_State *L) { + return lua_logger_print(L, true); + } + // end Lua stdout/stderr + + Script::Script(const ExecutionScope &scope) : scope(scope) { + state_lock.lock(); + + auto lst = luaL_newstate(); + auto entry = pair(lst, &scope); + state.insert(entry); + + this->lua_state = lst; + luaL_openlibs(this->lua_state); + + // Override print to send output to logger + lua_register(lua_state, "print", lua_logger_print); + lua_register(lua_state, "print_err", lua_logger_print_err); + + initialize(); + + state_lock.unlock(); + } + + Script::~Script() { + state_lock.lock(); + state.erase(this->lua_state); + lua_close(this->lua_state); + state_lock.unlock(); + } + + void Script::load_file(const string &script_file) { + auto status = luaL_loadfile(this->lua_state, script_file.c_str()); + + if (status) { + auto lua_error = lua_tostring(this->lua_state, -1); + throw ScriptEngineException("%s", lua_error); + } + } + + void Script::evaluate() { + int status = lua_pcall(this->lua_state, 0, LUA_MULTRET, 0); + + if (status) { + auto lua_error = lua_tostring(this->lua_state, -1); + throw ScriptEvaluationException("%s", lua_error); + } + } + + void Script::invoke_function_by_ref(const int reference) { + lua_rawgeti(this->lua_state, LUA_REGISTRYINDEX, reference); + int status = lua_pcall(this->lua_state, 0, 0, 0); + + if (status) { + auto lua_error = lua_tostring(this->lua_state, -1); + throw ScriptInvocationException("%s", lua_error); + } + } + + // BEGIN integration with Lua + lua_Debug get_lua_debug(lua_State *L) { + lua_Debug ar; + lua_getstack(L, 1, &ar); + lua_getinfo(L, "Sl", &ar); + return ar; + } + + int lua_api_level_require(lua_State *L) { + if (1 != lua_gettop(L) || !lua_isinteger(L, 1)) { + return luaL_error(L, "Expected one argument - api level as integer"); + } + + auto api_level_required = luaL_checkinteger(L, 1); + + if (api_level_required > LIBKAFE_API_LEVEL) { + return luaL_error( + L, + "Script requires minimum API level %d, libkafe API level is %d", + api_level_required, + LIBKAFE_API_LEVEL + ); + } + return 0; + } + + // TODO: allow kDSN format - + int lua_api_inventory_add(lua_State *L) { + auto scope = get_scope(L); + + if (5 != lua_gettop(L)) { + return luaL_error(L, "Expected five arguments - username, hostname, port, environment, role"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one must be a string - username"); + } + + if (!lua_isstring(L, 2)) { + return luaL_error(L, "Argument two must be a string - hostname"); + } + + if (!lua_isinteger(L, 3)) { + return luaL_error(L, "Argument three must be a integer - port"); + } + + if (!lua_isstring(L, 4)) { + return luaL_error(L, "Argument four must be a string - environment"); + } + + if (!lua_isstring(L, 5)) { + return luaL_error(L, "Argument five must be a string - role"); + } + + auto username = luaL_checkstring(L, 1); + auto hostname = luaL_checkstring(L, 2); + auto port = luaL_checkinteger(L, 3); + auto environment = luaL_checkstring(L, 4); + auto role = luaL_checkstring(L, 5); + + if (port < 1 || port > 65535) { + return luaL_error(L, "Invalid port value <%s>", port); + } + + auto item = new InventoryItem(username, hostname, (unsigned int) port, environment, role); + + auto inventory = const_cast(scope->get_inventory()); + + if (inventory->item_exists(*item)) { + return luaL_error(L, "Duplicate inventory item <%s>", item->to_string().c_str()); + } + + scope->get_context()->get_log_listener()->emit_info( + "Remote <%s+%s://%s@%s:%d> added to inventory", + item->get_environment().c_str(), + item->get_role().c_str(), + item->get_user().c_str(), + item->get_host().c_str(), + item->get_port() + ); + + inventory->add(*item); + + return 0; + } + + int lua_api_task_define(lua_State *L) { + auto scope = get_scope(L); + + if (2 != lua_gettop(L)) { + return luaL_error(L, "Expected two arguments, task name and task function"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one must be a string task name"); + } + + if (!lua_isfunction(L, 2)) { + return luaL_error(L, "Argument two must be a function"); + } + + auto task_name = luaL_checkstring(L, 1); + auto tasks = const_cast(scope->get_tasks()); + + if (tasks->task_exists(task_name)) { + auto other = tasks->get_task(task_name); + return luaL_error( + L, + "Duplicate task definition with name <%s>, already defined at %s", + task_name, + other->get_source().c_str() + ); + } + + auto function_reference = luaL_ref(L, LUA_REGISTRYINDEX); + + lua_Debug ar = get_lua_debug(L); + + auto source = string(ar.short_src) + + string(":") + + to_string(ar.currentline); + + auto task = new Task( + task_name, + function_reference, + source + ); + + tasks->add_task(*task); + + scope->get_context()->get_log_listener()->emit_info( + "Defined task <%s>", + task_name + ); + + return 0; + } + + int lua_api_on_role_invoke(lua_State *L) { + auto scope = get_scope(L); + auto *logger = const_cast(scope->get_context()->get_log_listener()); + + if (scope->has_current_api()) { + return luaL_error(L, "Nested role context invocation is not allowed (using kafe.on(...) when already " + "scoped by another kafe.on(...))"); + } + + auto n_args = lua_gettop(L); + if (2 != n_args && 3 != n_args) { + return luaL_error(L, + "Expected two or three arguments, environment name and function to execute, and optional skip flag"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one must be a string environment name"); + } + + if (!lua_isfunction(L, 2)) { + return luaL_error(L, "Argument two must be a function"); + } + + bool skip_empty = true; + + if (3 == n_args) { + if (!lua_isboolean(L, 3)) { + return luaL_error(L, "Argument three must be a boolean"); + } + + skip_empty = static_cast(lua_toboolean(L, 3)); + } + + auto role = luaL_checkstring(L, 1); + auto function_reference = luaL_ref(L, LUA_REGISTRYINDEX); + + auto inventory_items = scope->get_inventory()->find_for_scope( + scope->get_context()->get_environment(), + role + ); + + if (inventory_items.empty()) { + if (skip_empty) { + return 0; + } + + return luaL_error(L, "Unable to invoke method on role <%s> - has no targets", role); + } + + logger->emit_info("Entering role <%s>", string(role).c_str()); + logger->context_push(string(role)); + + bool failed = false; + for (auto item : inventory_items) { + auto remote_id = item->remote_id(); + logger->emit_info("Entering node <%s>", remote_id.c_str()); + logger->context_push(remote_id); + auto ssh_manager = SshManager(scope->get_ssh_pool(), item); + auto ssh_api = SshApi(&ssh_manager, logger); + + scope->set_current_remote(&ssh_api); + + lua_rawgeti(L, LUA_REGISTRYINDEX, function_reference); + int status = lua_pcall(L, 0, 0, 0); + + scope->clear_current_api(); + + if (status) { + failed = true; + auto lua_error = lua_tostring(L, -1); + logger->emit_warning("%s", lua_error); + logger->context_pop(); + break; + } + logger->context_pop(); + } + + logger->context_pop(); + + lua_pushboolean(L, !failed); + + return 1; + } + + int lua_api_remote_within(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not change remote directory when not in remote scope"); + } + + if (1 != lua_gettop(L) || !lua_isstring(L, 1)) { + luaL_error(L, "Expected one argument - string"); + } + + auto directory = scope->replace_vars(luaL_checkstring(L, 1)); + auto api = const_cast(scope->get_current_api()); + + scope->get_context()->get_log_listener()->emit_info( + "Changing remote working directory for current context to <%s>", + directory.c_str() + ); + + api->chdir(directory); + + return 0; + } + + int lua_api_remote_exec(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not execute remote command when not in remote scope"); + } + + int n_args = lua_gettop(L); + + if (1 != n_args && 2 != n_args) { + return luaL_error(L, "Expected one or two arguments"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + bool print_output = true; + if (n_args == 2) { + if (!lua_isboolean(L, 2)) { + return luaL_error(L, "Argument two is expected to be boolean"); + } + print_output = static_cast(lua_toboolean(L, 2)); + } + + auto command = scope->replace_vars(luaL_checkstring(L, 1)); + auto api = scope->get_current_api(); + + auto result = api->execute(command, print_output); + + lua_pushstring(L, result.get_stdout().c_str()); + lua_pushstring(L, result.get_stderr().c_str()); + lua_pushinteger(L, result.get_code()); + + return 3; + } + + int lua_api_remote_shell(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not execute remote command when not in remote scope"); + } + + int n_args = lua_gettop(L); + + if (1 != n_args) { + return luaL_error(L, "Expected one argument"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + auto command = scope->replace_vars(luaL_checkstring(L, 1)); + auto api = scope->get_current_api(); + + auto result = api->execute(command, true); + + lua_pushboolean(L, 0 == result.get_code()); + + return 1; + } + + int lua_api_archive_dir_tmp(lua_State *L) { + auto scope = get_scope(L); + + if (scope->has_current_api()) { + // TODO: issue warning + } + + if (1 != lua_gettop(L) || !lua_isstring(L, 1)) { + return luaL_error(L, "Expected one argument - string"); + } + + auto directory = scope->replace_vars(luaL_checkstring(L, 1)); + auto directory_norm = FileSystem::normalize(directory, scope->get_local_api()->get_chdir()); + + auto timer = scope->get_context()->get_log_listener()->emit_info_wt( + "Archiving directory <%s> into temporary archive", + directory_norm.c_str() + ); + + auto path = Archive::tmp_archive_from_directory(directory_norm); + + scope->get_context()->get_log_listener()->emit_success( + &timer, + "Archive created in <%s>", + path.c_str() + ); + + scope->add_rm_on_destruct(path); + + lua_pushstring(L, path.c_str()); + + return 1; + } + + int lua_api_archive_dir(lua_State *L) { + auto scope = get_scope(L); + + if (scope->has_current_api()) { + // TODO: issue warning + } + + if (2 != lua_gettop(L) || !lua_isstring(L, 1) || !lua_isstring(L, 2)) { + return luaL_error(L, "Expected two arguments - strings"); + } + + auto archive = scope->replace_vars(luaL_checkstring(L, 1)); + auto archive_norm = FileSystem::normalize(archive, scope->get_local_api()->get_chdir()); + auto directory = scope->replace_vars(luaL_checkstring(L, 2)); + auto directory_norm = FileSystem::normalize(directory, scope->get_local_api()->get_chdir()); + + auto timer = scope->get_context()->get_log_listener()->emit_info_wt( + "Archiving directory <%s> into archive file <%s>", + directory_norm.c_str(), + archive_norm.c_str() + ); + + Archive::archive_from_directory(archive_norm, directory_norm); + + scope->get_context()->get_log_listener()->emit_success( + &timer, + "Archive created in <%s>", + archive_norm.c_str() + ); + + return 0; + } + + int lua_api_upload_file(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not upload files when not in remote scope"); + } + + int n_args = lua_gettop(L); + + if (2 != n_args) { + return luaL_error(L, "Expected two arguments"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + if (!lua_isstring(L, 2)) { + return luaL_error(L, "Argument two is expected to be string"); + } + + auto local_file = scope->replace_vars(luaL_checkstring(L, 1)); + auto remote_file = scope->replace_vars(luaL_checkstring(L, 2)); + + auto local_file_norm = FileSystem::normalize(local_file, scope->get_local_api()->get_chdir()); + + auto api = scope->get_current_api(); + + auto timer = scope->get_context()->get_log_listener()->emit_info_wt( + "Uploading local file <%s> to remote <%s>", + local_file_norm.c_str(), + remote_file.c_str() + ); + + try { + api->scp_upload_file(local_file_norm, remote_file); + lua_pushboolean(L, true); + + scope->get_context()->get_log_listener()->emit_success( + &timer, + "Upload complete" + ); + lua_pushboolean(L, true); + } catch (exception &e) { + scope->get_context()->get_log_listener()->emit_error( + &timer, + "Upload failed - %s", + e.what() + ); + lua_pushboolean(L, false); + } + + return 1; + } + + int lua_api_download_file(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not download files when not in remote scope"); + } + + int n_args = lua_gettop(L); + + if (2 != n_args) { + return luaL_error(L, "Expected two arguments"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + if (!lua_isstring(L, 2)) { + return luaL_error(L, "Argument two is expected to be string"); + } + + auto local_file = scope->replace_vars(luaL_checkstring(L, 1)); + auto remote_file = scope->replace_vars(luaL_checkstring(L, 2)); + + auto local_file_norm = FileSystem::normalize(local_file, scope->get_local_api()->get_chdir()); + + auto api = scope->get_current_api(); + + auto timer = scope->get_context()->get_log_listener()->emit_info_wt( + "Downloading file <%s> from remote <%s>", + local_file_norm.c_str(), + remote_file.c_str() + ); + + try { + api->scp_download_file(local_file_norm, remote_file); + lua_pushboolean(L, true); + + scope->get_context()->get_log_listener()->emit_success( + &timer, + "Download complete" + ); + lua_pushboolean(L, true); + } catch (exception &e) { + scope->get_context()->get_log_listener()->emit_error( + &timer, + "Download failed - %s", + e.what() + ); + lua_pushboolean(L, false); + } + + return 1; + } + + int lua_api_upload_str(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not upload files when not in remote scope"); + } + + int n_args = lua_gettop(L); + + if (2 != n_args) { + return luaL_error(L, "Expected two arguments"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + if (!lua_isstring(L, 2)) { + return luaL_error(L, "Argument two is expected to be string"); + } + + auto content = scope->replace_vars(luaL_checkstring(L, 1)); + auto remote_file = scope->replace_vars(luaL_checkstring(L, 2)); + + auto api = scope->get_current_api(); + + auto timer = scope->get_context()->get_log_listener()->emit_info_wt( + "Uploading string as file to remote <%s>", + remote_file.c_str() + ); + + try { + api->scp_upload_file_from_string(content, remote_file); + lua_pushboolean(L, true); + + scope->get_context()->get_log_listener()->emit_success( + &timer, + "Upload complete" + ); + lua_pushboolean(L, true); + } catch (exception &e) { + scope->get_context()->get_log_listener()->emit_error( + &timer, + "Upload failed - %s", + e.what() + ); + lua_pushboolean(L, false); + } + + return 1; + } + + int lua_api_download_str(lua_State *L) { + auto scope = get_scope(L); + + if (!scope->has_current_api()) { + return luaL_error(L, "Can not download files when not in remote scope"); + } + + int n_args = lua_gettop(L); + + if (1 != n_args) { + return luaL_error(L, "Expected two arguments"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + auto remote_file = scope->replace_vars(luaL_checkstring(L, 1)); + auto api = scope->get_current_api(); + auto timer = scope->get_context()->get_log_listener()->emit_info_wt( + "Downloading string from remote <%s>", + remote_file.c_str() + ); + + try { + auto string = api->scp_download_file_as_string(remote_file); + lua_pushstring(L, string.c_str()); + + scope->get_context()->get_log_listener()->emit_success( + &timer, + "Download complete" + ); + } catch (exception &e) { + scope->get_context()->get_log_listener()->emit_error( + &timer, + "Download failed - %s", + e.what() + ); + lua_pushboolean(L, false); + } + + return 1; + } + + int lua_api_define(lua_State *L) { + auto scope = get_scope(L); + + int n_args = lua_gettop(L); + + if (2 != n_args) { + return luaL_error(L, "Expected two arguments"); + } + + auto key = luaL_checkstring(L, 1); + auto value = scope->replace_vars(lua_tostring(L, 2)); + + scope->get_context()->get_log_listener()->emit_debug( + "Defining variable <%s>, value <%s>", + key, + value.c_str() + ); + + scope->define_var(key, value); + + return 0; + } + + int lua_api_strfvars(lua_State *L) { + auto scope = get_scope(L); + int n_args = lua_gettop(L); + + if (1 != n_args) { + return luaL_error(L, "Expected one argument"); + } + + auto input = luaL_checkstring(L, 1); + + try { + lua_pushstring(L, scope->replace_vars(input).c_str()); + } catch (exception &e) { + return luaL_error(L, (string("Failed to format string - ") + string(e.what())).c_str()); + } + + return 1; + } + + int lua_api_strfenv(lua_State *L) { + auto scope = get_scope(L); + int n_args = lua_gettop(L); + + if (1 != n_args) { + return luaL_error(L, "Expected one argument"); + } + + auto input = luaL_checkstring(L, 1); + lua_pushstring(L, scope->replace_env(input).c_str()); + + return 1; + } + + int lua_api_local_within(lua_State *L) { + auto scope = get_scope(L); + + if (1 != lua_gettop(L) || !lua_isstring(L, 1)) { + luaL_error(L, "Expected one argument - string"); + } + + auto directory = scope->replace_vars(luaL_checkstring(L, 1)); + auto directory_norm = FileSystem::normalize(directory, scope->get_local_api()->get_chdir()); + + scope->get_context()->get_log_listener()->emit_info( + "Changing local working directory to <%s>", + directory_norm.c_str() + ); + + if (!FileSystem::is_directory(directory_norm)) { + return luaL_error(L, "Not a directory <%s>", directory_norm.c_str()); + } + + scope->get_local_api()->chdir(directory_norm); + + return 0; + } + + int lua_api_local_exec(lua_State *L) { + auto scope = get_scope(L); + + if (scope->has_current_api()) { + auto debug = get_lua_debug(L); + scope->get_context()->get_log_listener()->emit_warning( + "Executing local command with active remote scope in %s:%d. " + "Command will be executed for each server in context!", + debug.short_src, + debug.currentline + ); + } + + int n_args = lua_gettop(L); + + if (1 != n_args && 2 != n_args) { + return luaL_error(L, "Expected one or two arguments"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + bool print_output = true; + if (n_args == 2) { + if (!lua_isboolean(L, 2)) { + return luaL_error(L, "Argument two is expected to be boolean"); + } + print_output = static_cast(lua_toboolean(L, 2)); + } + + auto command = scope->replace_vars(luaL_checkstring(L, 1)); + auto result = scope->get_local_api()->local_popen(command, print_output); + + lua_pushstring(L, result.get_out().c_str()); + lua_pushinteger(L, result.get_code()); + + return 2; + } + + int lua_api_local_shell(lua_State *L) { + auto scope = get_scope(L); + + if (scope->has_current_api()) { + auto debug = get_lua_debug(L); + scope->get_context()->get_log_listener()->emit_warning( + "Executing local command with active remote scope in %s:%d. " + "Command will be executed for each server in context!", + debug.short_src, + debug.currentline + ); + } + + int n_args = lua_gettop(L); + + if (1 != n_args) { + return luaL_error(L, "Expected one argument"); + } + + if (!lua_isstring(L, 1)) { + return luaL_error(L, "Argument one is expected to be string"); + } + + auto command = scope->replace_vars(luaL_checkstring(L, 1)); + auto result = scope->get_local_api()->local_popen(command, true); + + lua_pushboolean(L, 0 == result.get_code()); + + return 1; + } + + extern "C" const struct luaL_Reg module_def[] = { + {"require_api", lua_api_level_require}, + {"task", lua_api_task_define}, + {"add_inventory", lua_api_inventory_add}, + {"on", lua_api_on_role_invoke}, + {"within", lua_api_remote_within}, + {"exec", lua_api_remote_exec}, + {"shell", lua_api_remote_shell}, + {"archive_dir_tmp", lua_api_archive_dir_tmp}, + {"archive_dir", lua_api_archive_dir}, + {"upload_file", lua_api_upload_file}, + {"download_file", lua_api_download_file}, + {"upload_str", lua_api_upload_str}, + {"download_str", lua_api_download_str}, + {"define", lua_api_define}, + {"strfvars", lua_api_strfvars}, + {"strfenv", lua_api_strfenv}, + {"local_exec", lua_api_local_exec}, + {"local_shell", lua_api_local_shell}, + {"local_within", lua_api_local_within}, + {nullptr, nullptr} + }; + + extern "C" int lua_module_init(lua_State *L) { +#if LUA_VERSION_NUM > 501 + lua_newtable(L); + luaL_setfuncs(L, module_def, 0); + + lua_pushstring(L, LIBKAFE_VERSION); + lua_setfield(L, -2, "version"); + + lua_pushinteger(L, LIBKAFE_VERSION_MAJOR); + lua_setfield(L, -2, "version_major"); + + lua_pushinteger(L, LIBKAFE_VERSION_MINOR); + lua_setfield(L, -2, "version_minor"); + + lua_pushinteger(L, LIBKAFE_VERSION_RELEASE); + lua_setfield(L, -2, "version_release"); + + lua_pushinteger(L, LIBKAFE_API_LEVEL); + lua_setfield(L, -2, "api_level"); + + lua_pushvalue(L, -1); +#else + luaL_register(L, LIBKAFE_LUA_MODULE_NAME, module_def); +#endif + return 1; + } + + extern "C" void lua_bootstrap(lua_State *L) { + luaL_requiref(L, LIBKAFE_LUA_MODULE_NAME, lua_module_init, false); + lua_pop(L, 1); + } + // END integration with Lua + + void Script::initialize() { + lua_bootstrap(this->lua_state); + } +} \ No newline at end of file diff --git a/packaging/COPYRIGHT b/packaging/COPYRIGHT new file mode 100644 index 0000000..7bdcc9f --- /dev/null +++ b/packaging/COPYRIGHT @@ -0,0 +1,15 @@ +Kafe project copyright notice + +Copyright 2020 Matiss Treinis + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file