├── .codeclimate.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── codeowners ├── pull_request_template.md └── security ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── docker-template ├── comp ├── bin ├── list └── list.pak ├── docker-template.gemspec ├── lib ├── docker │ ├── template.rb │ └── template │ │ ├── auth.rb │ │ ├── builder.rb │ │ ├── builder │ │ ├── normal.rb │ │ ├── rootfs.rb │ │ └── scratch.rb │ │ ├── cache.rb │ │ ├── cli.rb │ │ ├── cli │ │ ├── build.rb │ │ ├── list.rb │ │ └── run.rb │ │ ├── error.rb │ │ ├── logger.rb │ │ ├── meta.rb │ │ ├── notify.rb │ │ ├── parser.rb │ │ ├── repo.rb │ │ └── version.rb └── erb │ └── context.rb ├── script ├── install ├── rake.d │ └── update.rake ├── report ├── sync └── test ├── spec ├── rspec │ └── helper.rb ├── support │ ├── clear.rb │ ├── color.rb │ ├── contexts │ │ ├── docker.rb │ │ └── repos.rb │ ├── coverage.rb │ ├── filter.rb │ ├── matchers │ │ └── have_const.rb │ └── mocks │ │ ├── container.rb │ │ ├── image.rb │ │ └── repo.rb └── tests │ └── lib │ └── docker │ ├── template │ ├── auth_spec.rb │ ├── builder │ │ ├── normal_spec.rb │ │ ├── rootfs_spec.rb │ │ └── scratch_spec.rb │ ├── builder_spec.rb │ ├── cache_spec.rb │ ├── cli │ │ ├── build_spec.rb │ │ └── list_spec.rb │ ├── error_spec.rb │ ├── logger_spec.rb │ ├── meta_spec.rb │ ├── notify_spec.rb │ ├── parser_spec.rb │ └── repo_spec.rb │ └── template_spec.rb └── templates ├── rootfs.erb ├── rootfs ├── alpine.erb └── ubuntu.erb └── scratch.erb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | rubocop: 4 | enabled: false 5 | ratings: 6 | paths: 7 | - "spec/tests/**.rb" 8 | - "lib/**.rb" 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | end_of_line = lf 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report an Issue! 4 | --- 5 | 6 | 12 | 13 | - [ ] I tried updating to the latest version 14 | - [ ] I can't, there is an issue 15 | - [ ] This is about an < latest 16 | - [ ] I understand older versions may be unsupported 17 | - [ ] I Am on Windows 18 | - [ ] Ubuntu Bash on Windows 19 | - [ ] Fedora Bash on Windows 20 | - [ ] Other Bash on Windows 21 | - [ ] I Am on Linux 22 | - [ ] Ubuntu 23 | - [ ] Fedora 24 | - [ ] CentOS 25 | - [ ] Redhat 26 | - [ ] Debian 27 | - [ ] I am on macOS 10.13 28 | - [ ] I am on macOS 10.14 29 | - [ ] I'm on Docker 30 | - [ ] I understand Docker may be unsupported 31 | 32 | ## Description 33 | 34 | 40 | 41 | ## Steps 42 | 43 | - Step 1 44 | - Step 2 45 | - Step 3 46 | 47 | ## Output 48 | 49 | ```sh 50 | # Doing stuff. 51 | # Doing more stuff. 52 | # Oh no error. 53 | ``` 54 | 55 | ## Expected 56 | 57 | 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an Idea! 4 | --- 5 | 6 | - [ ] This feature is not on the latest version 7 | 8 | ## Request 9 | 10 | 15 | 16 | ## Examples 17 | 18 | ```sh 19 | # Your example 20 | # Maybe multiple 21 | # Examples? 22 | ``` 23 | -------------------------------------------------------------------------------- /.github/codeowners: -------------------------------------------------------------------------------- 1 | * @envygeeks 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - [ ] I have added or updated the specs/tests. 2 | - [ ] I have verified that the specs/tests pass on my computer. 3 | - [ ] I have not attempted to bump, or alter versions. 4 | - [ ] This is a documentation change. 5 | - [ ] This is a source change. 6 | 7 | ## Description 8 | 9 | 15 | -------------------------------------------------------------------------------- /.github/security: -------------------------------------------------------------------------------- 1 | * @envygeeks 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- 2 | # OS Level 3 | # -- 4 | .DS_Store 5 | 6 | # -- 7 | # Ruby 8 | # -- 9 | vendor/ 10 | coverage/ 11 | Gemfile.lock 12 | spec/fixture/site/ 13 | spec/fixture/result/ 14 | spec/fixture/out/ 15 | .bundle/ 16 | *.gem 17 | 18 | # -- 19 | # CodeClimate 20 | # -- 21 | cctr 22 | 23 | # -- 24 | # Jekyll 25 | # -- 26 | _site/ 27 | .jekyll-cache/ 28 | .jekyll-metadata 29 | .asset-cache/ 30 | site/ 31 | 32 | # -- 33 | # Rails 34 | # -- 35 | /db/*.sqlite3 36 | /npm-error.log 37 | .byebug_history 38 | /db/*.sqlite3-journal 39 | /config/application.yml 40 | /config/master.key 41 | /yarn-error.log 42 | /node_modules 43 | .pry_history 44 | /log 45 | /tmp 46 | 47 | # -- 48 | # Yarn 49 | # -- 50 | yarn.lock 51 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/envygeeks"] 2 | path = examples/envygeeks 3 | url = https://github.com/envygeeks/docker.git 4 | [submodule "examples/jekyll.git"] 5 | path = examples/jekyll 6 | url = https://github.com/jekyll/docker.git 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - &ruby1 2.4 3 | - &ruby2 2.5 4 | notifications: 5 | email: 6 | - jordon@envygeeks.io 7 | branches: 8 | only: 9 | - master 10 | addons: 11 | code_climate: 12 | repo_token: 13 | secure: "\ 14 | 3R+7bdlMXVNYtEI/3j+jycs1ENreTNny2sl19Too9VePSQyV2N1BCgyVqQU1H4Wj+5gS28\ 15 | 2852KEVVtLDEngux7cwn1NfEi8Jo9mTCtYzB8SV2/kct+s2+xOL+ctSxXqPFTeHlaPA3PK\ 16 | 93zx25Wcou64fNAZ7+xpegeWw901mIlLZfQ3uOAht7DENs+WKVzkpS3JwW9NUpSRpymk4d\ 17 | AnYmr03eg825i7XOjin9aE8oAeHzfalCJtpCFm0BhAVWGSmTVmItcLVaFVpVfhwG1P3grq\ 18 | wkyvZXFUBj2iZxuge2NzNabXMgOuoSOo3tQ8Abz1mgXLMtMuv+f63xVTEhHXTAK9QvEMLN\ 19 | LeAmi6ubjfLHZ90ue6JgiHDB+GpuhmtAfPlmPCByJkR33RCV3Y+ktAkEkaqOWQA6g8acAZ\ 20 | LJSoa2yiR7YcojF6C39ggd1Cy28HNd+PpRZzDJ2QJgu2JBWroZFukL13550Ir85isjb13X\ 21 | BBNmrzLNdR9LumYAMGpoeEAps8skE01QJCWcCrA6X73ykkGjLrhExqbe984DJJ9pisJ7vp\ 22 | PNwjdv8B95AXuE6ujflug/9LjUyevnGWPFmoRtgxj5CqzsyFRIthYMpZwFV6p0ff5Tq2Z6\ 23 | HgRmvWpZ8AVc/n2EiobOZ1zEAt+gzykmO8+1BuT/xDsFdAa/+NO0w=\ 24 | " 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | ## Our Pledge 3 | 4 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 5 | 6 | ## Our Standards 7 | 8 | Examples of behavior that contributes to creating a positive environment include: 9 | 10 | * Gracefully accepting constructive criticism 11 | * Being respectful of differing viewpoints and experiences 12 | * Showing empathy towards other community members 13 | * Focusing on what is best for the community 14 | * Using welcoming and inclusive language 15 | 16 | Examples of unacceptable behavior by participants include: 17 | 18 | * Trolling, insulting/derogatory comments, and personal or political attacks 19 | * Publishing private info, such as a physical, or electronic address, without explicit permission 20 | * Other conduct which could reasonably be considered inappropriate in a professional setting 21 | * Sexualized language, or imagery, and unwelcome sexual attention or advances 22 | * Public, or private harassment 23 | 24 | ## Our Responsibilities 25 | 26 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | ## Scope 29 | 30 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 31 | 32 | ## Enforcement 33 | 34 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at `jordon@envygeeks.io`. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 35 | 36 | ## Attribution 37 | 38 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4
39 | Available at [http://contributor-covenant.org/version/1/4][version] 40 | 41 | [homepage]: http://contributor-covenant.org 42 | [version]: http://contributor-covenant.org/version/1/4/ 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [@envygeeks]: https://github.com/envygeeks 2 | 3 | # Contributing 4 | 5 | **Hi!** *Thanks for considering a contribution to this project*. Below you will find a short set of guidelines to help you with your contribution. Be it a bug report, a pull request, or even a question. Let me also let you know that you don't have to file a pull-request, or contribute "raw code" to this project. I would never ask you to do that if you do not wish to, or currently are unable to. All I ask is that you file bug reports, and be respectful of the fact that this software is provided for free, on a donation basis, and at mostly [@envygeeks] expense via time (and sometimes cash.) 6 | 7 | ## Dependencies 8 | 9 | This project has an almost strict policy on dependencies. I do not do major updates (major version updates) of our current dependencies in anything but major releases, regardless of backwards compatibility. Dependencies are locked until the next major version. If there is a security reason for a major update, I ask that you file a bug with the upstream library and ask them to cut a minor release of the last release, which is proper. 10 | 11 | ***If they cannot provide a security update for the latest version, I will consider adding the major version as an optional version, but not restricting it as the only acceptable starting version, but I will complain about having to do so, because that's not cool.*** 12 | 13 | ### Languages 14 | 15 | When it comes to the language versions I support, if a language version becomes EOL (end-of-life) before a major release can be made, I will remove all official support for it before a major release, and update the list of supported versions inside of the testing. This is done to encourage users to keep updating quickly, rather than remaining on old, unstable, and unsupported versions of the language. 16 | 17 | #### Policies 18 | 19 | I tend to test on more than one version, and more than one back version of any given language if a major update hasn't been done in a while, and continue to support them as long as they are not end-of-life. For example, if my software starts with Ruby `2.2`, and the version is currently `2.5`, and I have no plans for a major update you can add `2.5` to the list of supported Rubies, however, the next major update will drop it to `2.4`, and `2.5`. If `2.2` becomes end-of-life, I will also drop it from the list. 20 | 21 | * **Ruby:** Latest + 1 Back 22 | * **JRuby:** Latest Release ***only*** 23 | * **Rubinius:** We do not support Rubinius at all. 24 | * **Node.js:** Latest LTS + Latest Release + 1 Back 25 | * **Go:** Latest + 1 Back 26 | 27 | ***It should be noted that if I wish to have a feature of a language before I can make a major release, I may, or may not go ahead and enforce a newer version of a language in a point release (exp: 3.x) so that I can update my code and clean it up.*** 28 | 29 | 30 | ## Bugs/Features 31 | 32 | ***If you do not wish to (or are unable to) file a pull-request, you can file a bug report for any issue you have...*** as long as it is not a question. Issues include *random results*, *random errors*, *segfaults*, or anything of that nature. When you do, please make sure to include the system you are working from, the version of the language or compiler you are working with, and any relevant information that can be used to replicate the issue. ***Unreplicable issues will be closed.*** ***You can (and are encouraged to) ask questions if they are about something not documented, or if there is a question about an ambiguity in the documentation, this will prompt me to update the documentation to fix the problem.*** 33 | 34 | ### What to not do 35 | 36 | * Ask me to put an urgency on your issue. 37 | * Be disrespectful: **I will block your comments**. 38 | * ":+1:" comments, I will lock issues; preventing further comments. 39 | * If you wish to "👍", "👎", or otherwise, please us the emoji-voting. 40 | * Ask if there are "any updates" 41 | 42 | **I do accept donations for fixing issues on a case-by-case urgency basis, as well as for creating features, if you need an issue addressed quickly. If you wish to do this you should contact [@envygeeks]. Otherwise issues are fixed based on complexity, time, and importance. All my projects get equal love, and sometimes it takes a minute to get back around.** 43 | 44 | ### Policies 45 | #### Closing 46 | * **Immediately:** `wontfix`, `stale`, `not-a-bug` 47 | * **Closed on Next Release:** `close-on-next-release` 48 | * **1 Week (7 Days)**: `pending-feedback` 49 | 50 | #### Fixing 51 | * **Bugfix x.x.X:** `bug`, `blocker` 52 | * **Minor x.X.x:** `non-blocker`, `bug`, `feature` 53 | * **Immediately:** `documentation` 54 | 55 | ## Pull Requests 56 | ### Tests 57 | 58 | If you change a method in any way, your tests ***must*** pass, and if there is an additional surface added, then you ***must add tests***, in Ruby I generally prefer to use RSpec, you should refer to RSpec's documentation for information on how it works, in Go, I prefer to use Go's native testing. 59 | 60 | * ***Ruby:*** `rake spec` 61 | * ***Go:*** `go test -cover -v` 62 | * `script/test` 63 | 64 | ### Code 65 | 66 | Code updates should follow the formatting of the given repository, formatting in Ruby is generally done via `rubocop` and is generally tied into `rake` and `rake spec`. Changes that are unrelated will be rejected and asked to be removed, regardless of your personal preference. You can always port those unrelated changes into another pull-request, unless they are arbitrary. 67 | 68 | #### Basics 69 | 70 | * Write your code in a clean and readable format. 71 | * Comment your code, at the high level, not lots of inline comments. 72 | * Stay at 80 lines if you can. 73 | 74 | ##### Good 75 | 76 | ```ruby 77 | # -- 78 | # @param hello [String] your greeting 79 | # Allows you to send a greeting back to the user 80 | # @raise if the given object is not a string 81 | # @return [String] the greeting 82 | # -- 83 | def greet(hello) 84 | raise ArgumentError, "needs string" unless hello.is_a?(String) 85 | alert hello, { 86 | class: ".greeting" 87 | } 88 | end 89 | ``` 90 | 91 | ##### Bad 92 | 93 | ```ruby 94 | # -- 95 | # @param hello [String] your greeting 96 | # Allows you to send a greeting back to the user 97 | # @return [String] the greeting 98 | # -- 99 | def greet(hello) 100 | # @raise if the given object is not a string 101 | raise ArgumentError, "needs string" unless hello.is_a?(String) 102 | # Ship it to the user 103 | alert hello, { 104 | class: ".greeting" 105 | } 106 | end 107 | ``` 108 | 109 | #### Commits 110 | 111 | Your pull-request should not add additional information outside of the Git-Commit. I understand this is Github, but explanitory data should remain in Git itself, not within Github (for the most part.) You should put the comment body of your pull request inside of your commit. 112 | 113 | ``` 114 | Message 115 | 116 | This pull-request solves X issue because it was really buggy. 117 | Please do not add extra `\n\n` here because Github displays it 118 | badly, so just let it flow, besides, this is what was intended for 119 | Git anyways, you only keep your message at 80c. 120 | ``` 121 | 122 | 123 | ### Documentation 124 | 125 | Documentation updates should follow the formatting of the given repository. You will be required to go through an approval process and even a comment processes. This process is mostly simple and should not impede a quick resolution. You should be prepared for the following: 126 | 127 | * Requests for unrelated changes to be removed. 128 | * Requests for language changes if the author feels it's ambiguous. 129 | * Requests for formatting changes. 130 | * Requests for `git squash` 131 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Frozen-string-literal: true 3 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 4 | # Encoding: utf-8 5 | # ---------------------------------------------------------------------------- 6 | 7 | source "https://rubygems.org" 8 | gem "rake", :require => false 9 | gemspec 10 | 11 | group :test do 12 | gem "rspec", :require => false 13 | gem "luna-rspec-formatters", :require => false 14 | gem "memory_profiler", :require => false, :platform => :mri 15 | gem "rugged", :require => false, :platform => :mri 16 | gem "benchmark-ips", :require => false 17 | gem "rspec-helpers", :require => false 18 | gem "simplecov", :require => false 19 | end 20 | 21 | group :development do 22 | unless ENV["CI"] 23 | gem "pry", :require => false 24 | gem "msgpack", { 25 | :require => false 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 - 2016 Jordon Bedwell 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Template 2 | 3 | [![Build](https://travis-ci.org/envygeeks/docker-template.svg?branch=master)][travis] 4 | 5 | [travis]: https://travis-ci.org/envygeeks/docker-template 6 | 7 | Docker Template is an organization and templating system for Docker images. A way to make your life easier and more organized by having repositories within repositories that share data among multiple sets of images. It is currently used to build all the images for Jekyll and EnvyGeeks. Please see https://github.com/envygeeks/docker-template/wiki for documentation. 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2017 - 2018 - MIT License 3 | # Source: https://github.com/envygeeks/devfiles 4 | # Author: Jordon Bedwell 5 | # Encoding: utf-8 6 | 7 | task default: [:spec] 8 | task(:spec) { exec "script/test" } 9 | task(:test) { exec "script/test" } 10 | Dir.glob("script/rake.d/*.rake").each do |v| 11 | load v 12 | end 13 | -------------------------------------------------------------------------------- /bin/docker-template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Frozen-string-literal: true 3 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 4 | # Encoding: utf-8 5 | 6 | STDOUT.sync = true 7 | STDERR.sync = true 8 | $LOAD_PATH.unshift(File.expand_path( 9 | "../lib", __dir__ 10 | )) 11 | 12 | # -- 13 | 14 | trap :SIGINT do 15 | $stderr.puts Simple::Ansi.red( 16 | "\nBye" 17 | ) 18 | 19 | exit 20 | end 21 | 22 | # -- 23 | 24 | begin 25 | require "docker/template/cli" 26 | rescue LoadError 27 | # Retry and let it fail if it doesn't work again. 28 | %w(bundler/setup docker/template/cli).each do |k| 29 | require k 30 | end 31 | end 32 | 33 | # Time to play the game. 34 | Docker::Template::CLI \ 35 | .start 36 | -------------------------------------------------------------------------------- /comp/bin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | _docker_template() { 3 | comp=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/list 4 | COMPREPLY=($(compgen -W "$($comp ${COMP_WORDS[@]})" -- \ 5 | ${COMP_WORDS[COMP_CWORD]})) 6 | } 7 | 8 | complete -F _docker_template \ 9 | docker-template 10 | -------------------------------------------------------------------------------- /comp/list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Frozen-string-literal: true 3 | # Copyright: 2016 Jordon Bedwell - MIT License 4 | # rubocop:disable Style/ElseAlignment 5 | # Encoding: utf-8 6 | 7 | ARGV.shift 8 | require "pathutil" 9 | require "msgpack" 10 | 11 | # -- 12 | 13 | list = MessagePack.unpack( 14 | Pathutil.new(__dir__).expand_path.join("list.pak").read 15 | ) 16 | 17 | # -- 18 | 19 | if ARGV.first == "build" 20 | dir = Pathutil.new("repos").expand_path 21 | 22 | if dir.directory? 23 | list["build"] ||= { "_reply" => [] } 24 | list["build"]["_reply"].push(*dir.children.map( 25 | &:basename 26 | )) 27 | end 28 | end 29 | 30 | # -- 31 | 32 | def key?(obj, key) 33 | obj["_reply"].include?( 34 | key 35 | ) 36 | end 37 | 38 | # -- 39 | 40 | def contains?(obj, key) 41 | result = obj["_reply"].grep(/#{Regexp.escape( 42 | key 43 | )}/) 44 | 45 | !result.empty? 46 | end 47 | 48 | # -- 49 | 50 | def opt?(key) 51 | key =~ /\A-{1,2}/ 52 | end 53 | 54 | # -- 55 | 56 | if ARGV.empty? 57 | $stdout.puts list["_reply"].join( 58 | " " 59 | ) 60 | else 61 | none = false 62 | rtrn = list 63 | 64 | ARGV.each_with_index do |key, index| 65 | if rtrn.key?(key) then rtrn = rtrn[key] 66 | elsif key?(rtrn, key) && !opt?(key) then none = true 67 | elsif index + 1 == ARGV.size && contains?(rtrn, key) then next 68 | elsif key?(rtrn, key) && opt?(key) then next 69 | else none = true 70 | end 71 | end 72 | 73 | unless none 74 | rtrn = rtrn["_reply"] 75 | $stdout.puts rtrn.join( 76 | " " 77 | ) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /comp/list.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/envygeeks/docker-template/236bb64f43cf97ba6b86b2027500820d4c6ef6aa/comp/list.pak -------------------------------------------------------------------------------- /docker-template.gemspec: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Frozen-string-literal: true 3 | # Copyright: 2015 Jordon Bedwell - Apache v2.0 License 4 | # Encoding: utf-8 5 | # ---------------------------------------------------------------------------- 6 | 7 | $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) 8 | require "docker/template/version" 9 | 10 | Gem::Specification.new do |spec| 11 | spec.authors = ["Jordon Bedwell"] 12 | spec.executables << "docker-template" 13 | spec.version = Docker::Template::VERSION 14 | spec.description = "Build and template awesome Docker images a variety of ways." 15 | spec.files = %W(Rakefile Gemfile README.md LICENSE) + Dir["{lib,bin,templates,comp}/**/*"] 16 | spec.summary = "Build and template Docker images a variety of ways." 17 | spec.homepage = "http://github.com/envygeeks/docker-template/" 18 | spec.required_ruby_version = ">= 2.1.0" 19 | spec.email = ["jordon@envygeeks.io"] 20 | spec.require_paths = ["lib"] 21 | spec.name = "docker-template" 22 | spec.license = "MIT" 23 | spec.bindir = "bin" 24 | 25 | spec.add_runtime_dependency("thor", "~> 0.19") 26 | spec.add_runtime_dependency("docker-api", "~> 1.28") 27 | spec.add_runtime_dependency("activesupport", ">= 4.2", "< 5.2") 28 | spec.add_runtime_dependency("simple-ansi", "~> 1.0") 29 | spec.add_runtime_dependency("pathutil", "~> 0.7") 30 | spec.add_runtime_dependency("extras", "~> 0.1") 31 | spec.add_runtime_dependency("json", ">= 1.8") 32 | end 33 | -------------------------------------------------------------------------------- /lib/docker/template.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "docker" 6 | require "extras/all" 7 | require "erb/context" 8 | require "forwardable/extended" 9 | require "simple/ansi" 10 | require "pathutil" 11 | require "set" 12 | 13 | # -- 14 | 15 | Excon.defaults[ :read_timeout] = 1440 16 | Excon.defaults[:write_timeout] = 1440 17 | 18 | # -- 19 | 20 | module Docker 21 | module Template 22 | module_function 23 | 24 | # -- 25 | 26 | def project? 27 | dir = root.join("docker") 28 | any = Builder.all.dup.keep_if(&:projects_allowed?) 29 | any = any.map(&:files).reduce(&:|).any? { |file| root.join(file).file? } 30 | return true if any && root.join(Meta.opts_file(:force => \ 31 | :project)).file? 32 | end 33 | 34 | # -- 35 | 36 | def root 37 | @root ||= begin 38 | Pathutil.new(Dir.pwd).realpath 39 | end 40 | end 41 | 42 | # -- 43 | 44 | def gem_root 45 | @gem_root ||= begin 46 | Pathutil.new("../../").expand_path( 47 | __dir__ 48 | ) 49 | end 50 | end 51 | 52 | # -- 53 | 54 | def template_root 55 | @template_root ||= begin 56 | gem_root.join("templates") 57 | end 58 | end 59 | 60 | # -- 61 | # Pull a `template` from the `template_root` to parse it's data. 62 | # TODO: Rename this to get_template! 63 | # -- 64 | 65 | def get(name, data = {}) 66 | data = ERB::Context.new(data) 67 | template = template_root.join("#{name}.erb").read unless name.is_a?(Pathutil) 68 | template = name.read if name.is_a?(Pathutil) 69 | template = ERB.new(template) 70 | 71 | return template.result( 72 | data._binding 73 | ) 74 | end 75 | 76 | # -- 77 | 78 | def _require(what) 79 | require what 80 | if block_given? 81 | yield true 82 | end 83 | rescue LoadError 84 | if block_given? 85 | yield false 86 | end 87 | end 88 | 89 | # -- 90 | 91 | def tmpdir 92 | if ENV["DOCKER_TEMPLATE_TMPDIR"] 93 | dir = Pathutil.new(ENV["DOCKER_TEMPLATE_TMPDIR"]) 94 | .tap(&:mkdir_p) 95 | else 96 | dir = root.join("tmp") 97 | if !dir.exist? 98 | # Make the directory and then throw it out at exit. 99 | dir.mkdir_p; ObjectSpace.define_finalizer(dir, proc do 100 | dir.rm_rf 101 | end) 102 | end 103 | 104 | dir 105 | end 106 | 107 | dir.realpath 108 | end 109 | end 110 | end 111 | 112 | # -- 113 | # Trick extras into merging array's into array's for us so users can inherit. 114 | # -- 115 | 116 | class Array 117 | def deep_merge(new_) 118 | self | new_ 119 | end 120 | end 121 | 122 | # -- 123 | 124 | require "docker/template/error" 125 | require "docker/template/cache" 126 | require "docker/template/notify" 127 | require "docker/template/builder" 128 | require "docker/template/logger" 129 | require "docker/template/parser" 130 | require "docker/template/repo" 131 | require "docker/template/meta" 132 | require "docker/template/auth" 133 | -------------------------------------------------------------------------------- /lib/docker/template/auth.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "open3" 6 | require "json" 7 | 8 | module Docker 9 | module Template 10 | class Auth 11 | DEFAULT_SERVER = "https://index.docker.io/v1/" 12 | def initialize(repo) 13 | @repo = repo 14 | end 15 | 16 | def auth_with_cmd? 17 | @repo.user =~ %r!/! 18 | end 19 | 20 | def auth_with_env? 21 | ENV.key?("DOCKER_USERNAME") && \ 22 | ENV.key?("DOCKER_PASSWORD") && \ 23 | ENV.key?("DOCKER_EMAIL") 24 | end 25 | 26 | # -- 27 | def auth(skip: nil) 28 | return auth_from_cmd if auth_with_cmd? && skip != :cmd 29 | return auth_from_env if auth_with_env? && skip != :env 30 | auth_from_config 31 | 32 | # Wrap around their error to create ours. 33 | rescue Docker::Error::AuthenticationError 34 | raise Error::UnsuccessfulAuth 35 | # Something went wrong? 36 | end 37 | 38 | # -- 39 | def auth_from_cmd 40 | case @repo.user 41 | when %r!^gcr\.io/! then auth_from_gcr 42 | else 43 | auth({ 44 | skip: :cmd 45 | }) 46 | end 47 | end 48 | 49 | # -- 50 | def auth_from_env 51 | Docker.authenticate!({ 52 | "username" => ENV["DOCKER_USERNAME"], 53 | "serveraddress" => ENV["DOCKER_SERVER"] || DEFAULT_SERVER, 54 | "password" => ENV["DOCKER_PASSWORD"], 55 | "email" => ENV["DOCKER_EMAIL"] 56 | }) 57 | end 58 | 59 | # -- 60 | def auth_from_config 61 | cred = Pathutil.new("~/.docker/config.json") 62 | cred = cred.expand_path.read_json 63 | 64 | unless cred.empty? 65 | cred["auths"].each do |server, info| 66 | next if info.empty? 67 | 68 | user, pass = Base64.decode64(info["auth"]).split(":", 2) 69 | Docker.authenticate!({ 70 | "username" => user, 71 | "serveraddress" => server, 72 | "email" => info["email"], 73 | "password" => pass 74 | }) 75 | end 76 | end 77 | end 78 | 79 | private 80 | def auth_from_gcr 81 | i, o, e, = Open3.popen3("docker-credential-gcr get") 82 | server, = @repo.user.split("/", 2) 83 | 84 | i.puts server; i.close 85 | val = JSON.parse(o.read.chomp) 86 | [o, e].map(&:close) 87 | 88 | if val 89 | Docker.authenticate!({ 90 | "serveraddress" => server, 91 | "username" => val["Username"], 92 | "email" => "docker-template+opensource@envygeeks.io", 93 | "password" => val["Secret"], 94 | }) 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/docker/template/builder.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class Builder 8 | extend Forwardable::Extended 9 | 10 | # -- 11 | 12 | attr_reader :repo 13 | attr_reader :context 14 | attr_reader :copy 15 | attr_reader :img 16 | 17 | # -- 18 | 19 | ALIAS_SETUP = [:cache_context] 20 | SETUP = [:setup_context, :copy_global, :copy_project, 21 | :copy_all, :copy_group, :copy_tag, :copy_cleanup, :copy_git, :build_context, 22 | :verify_context, :cache_context].freeze 23 | 24 | # -- 25 | 26 | def initialize(repo) 27 | @repo = repo 28 | end 29 | 30 | # -- 31 | # Checks to see if this repository is an alias. This happens when the 32 | # user has alised data inside of their configuration file. At this point 33 | # we will not only copy the parent's data but the aliased data. 34 | # -- 35 | 36 | def alias? 37 | !@repo.complex_alias? && @repo.alias? && !rootfs? 38 | end 39 | 40 | # -- 41 | 42 | def rootfs? 43 | is_a?( 44 | Rootfs 45 | ) 46 | end 47 | 48 | # -- 49 | 50 | def normal? 51 | @repo.type == "normal" \ 52 | && !rootfs? 53 | end 54 | 55 | # -- 56 | 57 | def scratch? 58 | @repo.type == "scratch" \ 59 | && !rootfs? 60 | end 61 | 62 | # -- 63 | 64 | def aliased_img 65 | if alias? 66 | then @aliased_img ||= begin 67 | Docker::Image.get( 68 | aliased_repo ? aliased_repo.to_s : aliased_tag 69 | ) 70 | end 71 | end 72 | 73 | rescue Docker::Error::NotFoundError 74 | if alias? 75 | nil 76 | end 77 | end 78 | 79 | # -- 80 | 81 | def push 82 | return if rootfs? || !@repo.pushable? 83 | 84 | Notify.push(self) 85 | Auth.new(@repo).auth 86 | img = @img || Image.get(@repo.to_s) 87 | img.push nil, :repo_tag => @repo.to_s, \ 88 | &Logger.new(repo).method(:api) 89 | 90 | rescue Docker::Error::NotFoundError 91 | $stderr.puts Simple::Ansi.red( 92 | "Image does not exist, unpushable." 93 | ) 94 | end 95 | 96 | # -- 97 | 98 | def build 99 | Simple::Ansi.clear if @repo.buildable? 100 | return build_alias if alias? 101 | setup 102 | 103 | if @repo.buildable? 104 | then Notify.build(@repo, :rootfs => rootfs?) do 105 | chdir_build 106 | end 107 | end 108 | 109 | push 110 | rescue SystemExit => exit_ 111 | teardown :img => true 112 | raise exit_ 113 | ensure 114 | if !rootfs? 115 | teardown else teardown({ 116 | :img => false 117 | }) 118 | end 119 | end 120 | 121 | # -- 122 | # This method is a default reference. It is called when the image is 123 | # done building or when there is an error and we need to clean up some 124 | # stuff before exiting, use it... please. 125 | # -- 126 | 127 | def teardown(*_) 128 | $stderr.puts Ansi.red( 129 | "#{__method__}: Not Implemented." 130 | ) 131 | end 132 | 133 | # -- 134 | # The prebuild happens when a user has "setup_context", which typically 135 | # only happens with scratch, which will prebuild it's rootfs image so 136 | # it can get to building it's actual image. 137 | # -- 138 | 139 | def setup 140 | unless respond_to?(:setup_context, true) 141 | raise Error::NoSetupContext 142 | end 143 | 144 | SETUP.map do |val| 145 | if respond_to?(val, true) 146 | send(val) 147 | end 148 | end 149 | end 150 | 151 | # -- 152 | 153 | private 154 | def build_alias 155 | alias_setup 156 | 157 | if @repo.buildable? 158 | if (repo = aliased_repo) 159 | aliased = self.class.new(repo) 160 | unless aliased_img 161 | aliased.build 162 | end 163 | 164 | elsif !aliased_img 165 | raise( 166 | Error::ImageNotFound, aliased_tag 167 | ) 168 | end 169 | 170 | Notify.alias(self) 171 | aliased_img.tag( 172 | @repo.to_tag_h 173 | ) 174 | end 175 | 176 | push 177 | end 178 | 179 | # -- 180 | 181 | private 182 | def alias_setup 183 | ALIAS_SETUP.map do |m| 184 | if respond_to?(m, true) 185 | send(m) 186 | end 187 | end 188 | end 189 | 190 | # -- 191 | 192 | private 193 | def chdir_build 194 | @context.chdir do 195 | logger = Logger.new(repo).method(:api) 196 | opts = { 197 | :force => @repo.meta.force?, 198 | :t => @repo.to_s(rootfs: rootfs?), 199 | :squash => @repo.meta.squash?, 200 | :nocache => @repo.meta.force? 201 | } 202 | 203 | if @repo.meta["tty"] 204 | $stderr.puts Simple::Ansi.yellow( 205 | "TTY not supported: Ignored." 206 | ) 207 | end 208 | 209 | @img = Docker::Image.build_from_dir(".", 210 | opts, &logger 211 | ) 212 | end 213 | end 214 | 215 | # -- 216 | 217 | private 218 | def cache_context 219 | if repo.cacheable? 220 | $stderr.puts Simple::Ansi.red( 221 | "Context caching not supported" 222 | ) 223 | end 224 | end 225 | 226 | # -- 227 | # Copy any git repositories the user wishes us to copy. 228 | # -- 229 | 230 | private 231 | def copy_git 232 | return if rootfs? || !@repo.meta.git? || @repo.meta.push_only? 233 | require "rugged" 234 | 235 | repos = @repo.meta[:git] 236 | repos = repos.for_all + (repos.by_tag || []) + 237 | (repos.by_type || []) 238 | 239 | repos.each do |repo| 240 | credentials = Rugged::Credentials::SshKey.new({ 241 | :privatekey => Pathutil.new(repo[:key]).expand_path.to_s, 242 | :publickey => Pathutil.new(repo[:pub]).expand_path.to_s, 243 | :username => repo[:user] 244 | }) 245 | 246 | dir = @copy.join(repo[:clone_to]) 247 | if !dir.exist? 248 | $stderr.puts Simple::Ansi.green("Cloning #{repo[:repo]} to #{repo[:clone_to]}.") 249 | Rugged::Repository.clone_at(repo[:repo], dir.to_s, { 250 | :credentials => credentials 251 | }) 252 | else 253 | $stderr.puts Simple::Ansi.yellow( 254 | "Skipping #{repo[:repo]}, exists already." 255 | ) 256 | end 257 | end 258 | end 259 | 260 | # -- 261 | # The root can have it's own global copy directory shared across all 262 | # repos in your repo container dir so this encapsulates those. 263 | # -- 264 | 265 | private 266 | def copy_global 267 | unless rootfs? 268 | dir = Template.root.join( 269 | @repo.meta["copy_dir"] 270 | ) 271 | 272 | if dir.exist? 273 | then dir.safe_copy( 274 | @copy, :root => Template.root 275 | ) 276 | end 277 | end 278 | end 279 | 280 | # -- 281 | 282 | private 283 | def copy_project 284 | if Template.project? 285 | ignores = repo.meta["project_copy_ignore"].map do |path| 286 | Pathutil.new(path).expand_path( 287 | Template.root 288 | ) 289 | end 290 | 291 | Template.root.safe_copy( 292 | context.join(repo.meta.project_copy_dir), { 293 | :root => Template.root, :ignore => ignores 294 | } 295 | ) 296 | end 297 | end 298 | 299 | # -- 300 | 301 | private 302 | def copy_tag 303 | unless rootfs? 304 | dir = @repo.copy_dir("tag", @repo.tag) 305 | 306 | if dir.exist? 307 | then dir.safe_copy( 308 | @copy, :root => Template.root 309 | ) 310 | end 311 | end 312 | end 313 | 314 | # -- 315 | 316 | private 317 | def copy_group 318 | build_group = @repo.meta["tags"][ 319 | @repo.tag 320 | ] 321 | 322 | unless rootfs? || !build_group 323 | dir = @repo.copy_dir("group", build_group) 324 | 325 | if dir.exist? 326 | then dir.safe_copy( 327 | @copy, :root => Template.root 328 | ) 329 | end 330 | end 331 | end 332 | 333 | # -- 334 | 335 | private 336 | def copy_all 337 | unless rootfs? 338 | dir = @repo.copy_dir("all") 339 | 340 | if dir.exist? 341 | then dir.safe_copy( 342 | @copy, :root => Template.root 343 | ) 344 | end 345 | end 346 | end 347 | 348 | # -- 349 | 350 | rb_delegate :aliased_tag, :to => "repo.meta" 351 | rb_delegate :aliased_repo, { 352 | :to => :repo, :alias_of => :aliased 353 | } 354 | 355 | class << self 356 | 357 | # -- 358 | # REFERENCE METHOD: This is here to let you know we access files. 359 | # -- 360 | 361 | def files 362 | return [ 363 | # 364 | ] 365 | end 366 | 367 | # -- 368 | 369 | def projects_allowed! 370 | return @projects_allowed \ 371 | = true 372 | end 373 | 374 | # -- 375 | 376 | def projects_allowed? 377 | return !!@projects_allowed 378 | end 379 | 380 | # -- 381 | 382 | def sub? 383 | false 384 | end 385 | 386 | # -- 387 | 388 | def inherited(klass) 389 | (@sub_classes ||= []).push( 390 | klass 391 | ) 392 | end 393 | 394 | # -- 395 | 396 | def all 397 | @sub_classes ||= [ 398 | # 399 | ] 400 | end 401 | end 402 | end 403 | end 404 | end 405 | 406 | require "docker/template/builder/rootfs" 407 | require "docker/template/builder/scratch" 408 | require "docker/template/builder/normal" 409 | -------------------------------------------------------------------------------- /lib/docker/template/builder/normal.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class Builder 8 | class Normal < Builder 9 | projects_allowed! 10 | 11 | def teardown(img: false) 12 | @img.delete "force" => true if @img && img 13 | @context.rmtree if @context && \ 14 | @context.directory? 15 | end 16 | 17 | # -- 18 | 19 | def setup_context 20 | @context = @repo.tmpdir 21 | @copy = @context.join("copy") 22 | copy_dockerfile 23 | @copy.mkdir 24 | end 25 | 26 | # -- 27 | 28 | private 29 | def copy_dockerfile 30 | dockerfile = Template.project?? Template.root : @repo.root 31 | dockerfile = dockerfile.join("Dockerfile").read 32 | 33 | data = ERB::Context.new(:meta => @repo.meta) 34 | data = ERB.new(dockerfile).result(data._binding) 35 | context = @context.join("Dockerfile") 36 | context.write(data) 37 | end 38 | 39 | # -- 40 | 41 | private 42 | def cache_context 43 | return unless @repo.cacheable? 44 | return Cache.aliased_context(self) if alias? 45 | Cache.context(self, @context) 46 | end 47 | 48 | class << self 49 | def files 50 | %w( 51 | Dockerfile 52 | ) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/docker/template/builder/rootfs.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class Builder 8 | class Rootfs < Builder 9 | extend Forwardable::Extended 10 | 11 | # -- 12 | 13 | def data 14 | Template.get(:rootfs, { 15 | :rootfs_base_img => @repo.meta["rootfs_base_img"] 16 | }) 17 | end 18 | 19 | # -- 20 | 21 | def discover 22 | self.class.files.map do |file| 23 | file = @repo.root.join( 24 | file 25 | ) 26 | 27 | if file.file? 28 | then return file 29 | end 30 | end 31 | 32 | "rootfs/#{ 33 | @repo.meta.rootfs_template 34 | }" 35 | end 36 | 37 | # -- 38 | 39 | def builder_data 40 | Template.get(discover, { 41 | :meta => @repo.meta 42 | }) 43 | end 44 | 45 | # -- 46 | # During a simple copy you store all the data (including rootfs) data 47 | # as a project unit, this helps us clean up data that is known to be for 48 | # just the rootfs image and remove it so it doesn't impact. 49 | # -- 50 | def simple_cleanup(dir) 51 | file = dir.join("usr/local/bin/mkimg") 52 | 53 | if file.exist? 54 | then file.delete 55 | end 56 | end 57 | 58 | # -- 59 | 60 | def teardown(img: true) 61 | @context.rmtree if @context && @context.directory? 62 | @img.delete "force" => true if @img && img \ 63 | rescue nil 64 | end 65 | 66 | # -- 67 | 68 | private 69 | def setup_context 70 | @context = @repo.tmpdir("rootfs") 71 | @copy = @context.join(@repo.meta["copy_dir"]) 72 | @context.join("Dockerfile").write(data) 73 | 74 | @copy.join("usr/local/bin").mkdir_p 75 | @copy.join("usr/local/bin/mkimg").write(builder_data) 76 | @copy.join("usr/local/bin/mkimg").chmod(0755) 77 | copy_rootfs 78 | end 79 | 80 | # -- 81 | 82 | private 83 | def copy_rootfs 84 | dir = @repo.copy_dir( 85 | "rootfs" 86 | ) 87 | 88 | if dir.exist? 89 | @repo.copy_dir("rootfs").safe_copy(@copy, { 90 | :root => Template.root 91 | }) 92 | end 93 | end 94 | 95 | class << self 96 | def sub? 97 | return true 98 | end 99 | 100 | # -- 101 | 102 | def files 103 | %w( 104 | Rootfs.erb Rootfs rootfs.erb rootfs 105 | ) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/docker/template/builder/scratch.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class Builder 8 | class Scratch < Builder 9 | attr_reader :rootfs 10 | 11 | # -- 12 | 13 | def initialize(*args) 14 | super; @rootfs = Rootfs.new( 15 | repo 16 | ) 17 | end 18 | 19 | # -- 20 | 21 | def data 22 | Template.get(:scratch, { 23 | :entrypoint => @repo.meta.entry, 24 | :maintainer => @repo.meta.maintainer, 25 | :tar_gz => @tar_gz.basename 26 | }) 27 | end 28 | 29 | # -- 30 | 31 | def teardown(img: false) 32 | @copy.rm_rf if @copy 33 | @context.rm_rf if @context 34 | @tar_gz.rm_rf if @tar_gz 35 | 36 | if @img && img 37 | then @img.delete({ 38 | "force" => true 39 | }) 40 | end 41 | rescue Docker::Error::NotFoundError 42 | nil 43 | end 44 | 45 | # -- 46 | 47 | private 48 | def setup_context 49 | @context = @repo.tmpdir 50 | @tar_gz = @repo.tmpfile "archive", ".tar.gz", root: @context 51 | @copy = @repo.tmpdir "copy" 52 | copy_dockerfile 53 | end 54 | 55 | # -- 56 | 57 | private 58 | def copy_dockerfile 59 | data = self.data % @tar_gz.basename 60 | dockerfile = @context.join("Dockerfile") 61 | dockerfile.write(data) 62 | end 63 | 64 | # -- 65 | 66 | def copy_cleanup 67 | @rootfs.simple_cleanup( 68 | @copy 69 | ) 70 | end 71 | 72 | # -- 73 | 74 | def verify_context 75 | if @repo.buildable? && @tar_gz.zero? 76 | raise Error::InvalidTargzFile, @tar_gz 77 | end 78 | end 79 | 80 | # -- 81 | 82 | private 83 | def build_context 84 | return unless @repo.buildable? 85 | @rootfs.build 86 | 87 | logger = Logger.new 88 | img = Container.create(create_args) 89 | img.start.attach(logger_opts, &logger.method(logger_type)) 90 | status = img.json["State"]["ExitCode"] 91 | 92 | if status != 0 93 | logger.simple(:stderr, img.logs(:stderr => true)) unless logger.output? 94 | logger.simple(:stdout, img.logs(:stdout => true)) unless logger.output? 95 | raise Error::BadExitStatus, status 96 | end 97 | ensure 98 | if img 99 | then img.tap(&:stop).delete({ 100 | "force" => true 101 | }) 102 | end 103 | 104 | @rootfs.teardown 105 | end 106 | 107 | # -- 108 | 109 | private 110 | def logger_type 111 | @repo.meta["tty"] ? :tty : :simple 112 | end 113 | 114 | # -- 115 | 116 | private 117 | def logger_opts 118 | return { 119 | :tty => @repo.meta["tty"], :stdout => true, :stderr => true 120 | } 121 | end 122 | 123 | # -- 124 | 125 | private 126 | def create_args 127 | name = ["rootfs", @repo.name, @repo.tag, "image"].join("-") 128 | env = @repo.to_env(:tar_gz => @tar_gz, :copy_dir => @copy) 129 | 130 | return { 131 | "Env" => env.to_a, 132 | "Tty" => @repo.meta["tty"], 133 | "Image" => @rootfs.img.id, 134 | "Name" => name, 135 | 136 | "HostConfig" => { 137 | "Binds" => [ 138 | "#{@copy}:#{@copy}", "#{@tar_gz}:#{@tar_gz}" 139 | ] 140 | }, 141 | 142 | "Volumes" => { 143 | @copy.to_s => { 144 | "source" => @copy.to_s, 145 | "destination" => @copy.to_s 146 | }, 147 | 148 | @tar_gz.to_s => { 149 | "source" => @tar_gz.to_s, 150 | "destination" => @tar_gz.to_s 151 | } 152 | } 153 | } 154 | end 155 | 156 | class << self 157 | def files 158 | return Rootfs \ 159 | .files 160 | end 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/docker/template/cache.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | module Cache 8 | module_function 9 | 10 | # -- 11 | # Cache the context into the cache directory. 12 | # -- 13 | 14 | def context(builder, context) 15 | builder.repo.cache_dir.rm_rf 16 | $stderr.puts Simple::Ansi.yellow(format("Copying context for %s", builder.repo)) 17 | cache_dir = builder.repo.cache_dir 18 | cache_dir.parent.mkdir_p 19 | 20 | context.cp_r(cache_dir.tap( 21 | &:rm_rf 22 | )) 23 | 24 | readme(builder) 25 | end 26 | 27 | # -- 28 | # rubocop:disable Metrics/LineLength 29 | # -- 30 | 31 | def aliased_context(builder) 32 | if builder.aliased_repo.cache_dir.exist? 33 | $stderr.puts Simple::Ansi.yellow(format("Copying %s context to %s", builder.aliased_repo, builder.repo)) 34 | builder.aliased_repo.cache_dir.cp_r(builder.repo.cache_dir.tap( 35 | &:rm_rf 36 | )) 37 | end 38 | end 39 | 40 | # -- 41 | # Cleanup the context caches, removing the caches we no longer need. 42 | # rubocop:enable Metrics/LineLength 43 | # -- 44 | 45 | def cleanup(repo) 46 | return unless repo.clean_cache? 47 | cache_dir = repo.cache_dir.parent 48 | 49 | if cache_dir.exist? 50 | cache_dir.children.each do |file| 51 | next unless repo.meta.tags.include?(file.basename) 52 | $stdout.puts Simple::Ansi.yellow(format("Removing %s.", 53 | file.relative_path_from(Template.root) 54 | )) 55 | 56 | file.rm_rf 57 | end 58 | end 59 | end 60 | 61 | # -- 62 | # Note: We normally expect but do not require you to have a README. 63 | # Search for and copy the readme if available. 64 | # -- 65 | 66 | def readme(builder) 67 | return unless file = builder.repo.root.children.find { |val| val =~ /readme/i } 68 | file.safe_copy(builder.repo.cache_dir, { 69 | :root => file.parent 70 | }) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/docker/template/cli.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "docker/template" 6 | require "thor" 7 | 8 | module Docker 9 | module Template 10 | class CLI < Thor 11 | 12 | # -- 13 | 14 | option :force, :type => :boolean, :desc => "Force caching." 15 | desc "cache [REPOS [OPTS]]", "Cache all (or some) of your repositories." 16 | option :clean, :type => :boolean, :desc => "Cleanup your caches." 17 | option :help, :type => :boolean, :desc => "Output this." 18 | 19 | # -- 20 | 21 | def cache(*args) 22 | return help(__method__) if options.help? 23 | self.options = options.merge(:cache => true) if options.force? 24 | self.options = options.merge(:cache_only => true) 25 | return build( 26 | *args 27 | ) 28 | end 29 | 30 | # -- 31 | 32 | option :force, :type => :boolean, :desc => "Force cleaning." 33 | desc "clean [REPOS [OPTS]]", "Clean all (or some) of your repositories caches." 34 | option :help, :type => :boolean, :desc => "Output this." 35 | 36 | # -- 37 | 38 | def clean(*args) 39 | return help(__method__) if options.help? 40 | self.options = options.merge(:clean => true) if options.force? 41 | self.options = options.merge(:clean_only => true) 42 | return build( 43 | *args 44 | ) 45 | end 46 | 47 | # -- 48 | 49 | option :force, :type => :boolean, :desc => "Force cleaning." 50 | desc "push [REPOS [OPTS]]", "Push all (or some) of your repositories." 51 | option :help, :type => :boolean, :desc => "Output this." 52 | 53 | # -- 54 | 55 | def push(*args) 56 | return help(__method__) if options.help? 57 | self.options = options.merge(:push => true) if options.force? 58 | self.options = options.merge(:push_only => true) 59 | return build( 60 | *args 61 | ) 62 | end 63 | 64 | # -- 65 | # docker-template build [repos [opts]] 66 | # -- 67 | 68 | desc "build [REPOS [OPTS]]", "Build all (or some) of your repositories." 69 | option :profile, :type => :boolean, :desc => "Profile Memory." 70 | option :tty, :type => :boolean, :desc => "Enable TTY Output." 71 | option :cache, :type => :boolean, :desc => "Cache your repositories to cache." 72 | option :exclude, :type => :array, :desc => "Build everything except for these images." 73 | option :debug, :type => :boolean, :desc => "Send the DEBUG=true env var to your instance." 74 | option :diff, :type => :boolean, :desc => "Build only modified repositories." 75 | option :push, :type => :boolean, :desc => "Push Repo After Building." 76 | option :clean, :type => :boolean, :desc => "Cleanup your caches." 77 | option :force, :type => :boolean, :desc => "Force your build." 78 | option :squash, :type => :boolean, :desc => "Squash the build." 79 | option :help, :type => :boolean, :desc => "Output this." 80 | 81 | # -- 82 | # rubocop:disable Lint/RescueException 83 | # -- 84 | 85 | def build(*args) 86 | return help(__method__) if options.help? 87 | Build.new(args, options) 88 | .start 89 | 90 | rescue Docker::Template::Error::StandardError => e 91 | $stderr.puts Simple::Ansi.red(e.message) 92 | exit e.respond_to?(:status) ? \ 93 | e.status : 1 94 | 95 | rescue Exception => _e 96 | raise unless $ERROR_POSITION 97 | $ERROR_POSITION.delete_if do |source| 98 | source =~ %r!#{Regexp.escape( 99 | __FILE__ 100 | )}!o 101 | end 102 | end 103 | 104 | # -- 105 | # rubocop:enable Lint/RescueException 106 | # docker-template list [options] 107 | # -- 108 | 109 | option :help, :type => :boolean, :desc => "Output this." 110 | desc "list [OPTS]", "List all possible builds." 111 | 112 | # -- 113 | 114 | def list 115 | return help(__method__) if options.help? 116 | return $stdout.puts( 117 | List.build 118 | ) 119 | end 120 | end 121 | end 122 | end 123 | 124 | require "docker/template/cli/build" 125 | require "docker/template/cli/list" 126 | -------------------------------------------------------------------------------- /lib/docker/template/cli/build.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class CLI 8 | class Build 9 | def initialize(args, opts) 10 | @opts = Meta.new(opts || {}) 11 | @repos = Parser.new(args, opts || {}).parse 12 | @args = args 13 | end 14 | 15 | # -- 16 | 17 | def start 18 | _profile do 19 | changed! if @opts.diff? 20 | exclude! if @opts.exclude? 21 | @repos.tap { |o| o.map(&:build) }.uniq(&:name).map( 22 | &:clean 23 | ) 24 | end 25 | end 26 | 27 | # -- 28 | 29 | def exclude! 30 | Parser.new(@opts[:exclude].map { |v| v.split(/,\s*/) }.flatten.compact).parse.each do |repo| 31 | @repos.delete_if do |v| 32 | v.name == repo.name && v.tag == repo.tag 33 | end 34 | end 35 | end 36 | 37 | # -- 38 | # rubocop:disable Metrics/AbcSize 39 | # -- 40 | 41 | def changed! 42 | Template._require "rugged" do |loaded| 43 | return true unless loaded 44 | 45 | git = Rugged::Repository.new(Template.root.to_s) 46 | dir = Template.root.join(@opts.repos_dir) 47 | 48 | repos = git.last_commit.diff.each_delta.each_with_object(Set.new) do |delta, set| 49 | next unless Pathutil.new(delta.new_file[:path]).expand_path(Template.root).in_path?(dir) 50 | set.merge(delta.new_file[:path].split("/").values_at( 51 | 1 52 | )) 53 | end 54 | 55 | @repos = @repos.select do |repo| 56 | repos.include?( 57 | repo.name 58 | ) 59 | end 60 | end 61 | end 62 | 63 | # -- 64 | # rubocop:enable Metrics/AbcSize 65 | # -- 66 | 67 | private 68 | def _profile 69 | return yield unless @opts.profile? 70 | Template._require "memory_profiler" do 71 | profiler = MemoryProfiler.report(:top => 10_240) { yield } 72 | profiler.pretty_print({ 73 | :to_file => "profile.txt" 74 | }) 75 | end 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/docker/template/cli/list.rb: -------------------------------------------------------------------------------- 1 | module Docker 2 | module Template 3 | class CLI 4 | class List 5 | def self.build 6 | return new.build 7 | end 8 | 9 | # -- 10 | 11 | def initialize(images = Parser.new([], {}).parse) 12 | @images = images 13 | end 14 | 15 | # -- 16 | 17 | def build 18 | out = "" 19 | 20 | @images.group_by(&:user).each do |user, images| 21 | out += "[user] " + Simple::Ansi.blue(user) + "\n" + repos( 22 | user, images 23 | ) 24 | end 25 | 26 | out 27 | end 28 | 29 | # -- 30 | 31 | def repos(user, images) 32 | out = "" 33 | 34 | images.group_by(&:name).each do |name, _| 35 | out += " ├─ [repo] " + Simple::Ansi.green(name) + "\n" 36 | out += tags(user, name, images) 37 | out += remote_aliases( 38 | user, name, images 39 | ) 40 | end 41 | 42 | out 43 | end 44 | 45 | # -- 46 | 47 | def tags(user, name, images) 48 | out = "" 49 | 50 | images.select { |image| image.name == name && image.user == user && !image.alias? }.each do |image| 51 | out += " │ ├─ [tag] " + Simple::Ansi.magenta(image.tag) + "\n" 52 | out += aliases( 53 | user, name, image.tag, images 54 | ) 55 | end 56 | 57 | out 58 | end 59 | 60 | # -- 61 | 62 | def remote_aliases(*args) 63 | out = "" 64 | 65 | remotes = _remote_aliases(*args).group_by do |image| 66 | image.meta[:aliases][ 67 | image.tag 68 | ] 69 | end 70 | 71 | remotes.each do |remote, images_| 72 | out += " │ ├─ [remote] " 73 | out += Simple::Ansi.yellow(remote) 74 | out += "\n" 75 | 76 | images_.each do |image| 77 | out += " │ │ ├─ [alias] " 78 | out += Simple::Ansi.yellow( 79 | image.tag 80 | ) 81 | 82 | out += "\n" 83 | end 84 | end 85 | 86 | out 87 | end 88 | 89 | # -- 90 | 91 | def _remote_aliases(user, name, images) 92 | images.select do |image| 93 | image.user == user && image.name == name && aliased_remote?( 94 | image 95 | ) 96 | end 97 | end 98 | 99 | # -- 100 | 101 | def aliases(user, name, tag, images, depth: 0) 102 | out = "" 103 | 104 | _aliases(user, name, tag, images).each do |alias_| 105 | name_ = \ 106 | if alias_.name == name 107 | Simple::Ansi.yellow( 108 | alias_.tag 109 | ) 110 | 111 | else 112 | Simple::Ansi.yellow( 113 | "#{alias_.name}:#{alias_.tag}" 114 | ) 115 | end 116 | 117 | out += " │ │ #{"│ " * depth}├─ [alias] #{name_}\n" 118 | out += aliases(user, name, alias_.tag, images, { 119 | :depth => depth + 1 120 | }) 121 | end 122 | 123 | out 124 | end 125 | 126 | # -- 127 | 128 | private 129 | def aliased_remote?(image) 130 | return image.alias? && !image.aliased 131 | end 132 | 133 | # -- 134 | 135 | private 136 | def _aliases(user, name, tag, images) 137 | images.select do |image| 138 | image.alias? && image.aliased && image.aliased.tag == tag \ 139 | && image.aliased.name == name && image.aliased.user \ 140 | == user 141 | end 142 | end 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/docker/template/cli/run.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2017 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class CLI 8 | class Run 9 | def initialize(args, opts) 10 | @opts = Meta.new(opts || {}) 11 | @repos = Parser.new(args, opts || {}).parse 12 | @args = args 13 | end 14 | 15 | # -- 16 | 17 | def start 18 | _profile do 19 | @repos.tap do |o| 20 | o.map do |r| 21 | r.template 22 | $stdout.puts( 23 | r.tmpdir 24 | ) 25 | end 26 | end 27 | end 28 | end 29 | 30 | # -- 31 | # rubocop:enable Metrics/AbcSize 32 | # -- 33 | 34 | private 35 | def _profile 36 | return yield unless @opts.profile? 37 | Template._require "memory_profiler" do 38 | profiler = MemoryProfiler.report(:top => 10_240) { yield } 39 | profiler.pretty_print({ 40 | :to_file => "profile.txt" 41 | }) 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/docker/template/error.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | module Error 8 | StandardError = Class.new( 9 | StandardError 10 | ) 11 | 12 | # -- 13 | 14 | class UnsuccessfulAuth < StandardError 15 | def initialize 16 | super "Unable to authorize you to Dockerhub, something is wrong." 17 | end 18 | end 19 | 20 | # -- 21 | 22 | class BadExitStatus < StandardError 23 | attr_reader :status 24 | 25 | def initialize(status) 26 | super "Got bad exit status #{ 27 | @status = status 28 | }" 29 | end 30 | end 31 | 32 | # -- 33 | 34 | class BadRepoName < StandardError 35 | def initialize(name) 36 | super "Only a-z0-9_- are allowed. Invalid repo name: #{ 37 | name 38 | }" 39 | end 40 | end 41 | 42 | # -- 43 | 44 | class InvalidTargzFile < StandardError 45 | def initialize(tar_gz) 46 | super "No data was given to the tar.gz file '#{ 47 | tar_gz.basename 48 | }'" 49 | end 50 | end 51 | 52 | # -- 53 | 54 | class InvalidYAMLFile < StandardError 55 | def initialize(file) 56 | super "The yaml data provided by #{file} is invalid and not a hash." 57 | end 58 | end 59 | 60 | # -- 61 | 62 | class NoSetupContext < StandardError 63 | def initialize 64 | super "No #setup_context method exists." 65 | end 66 | end 67 | 68 | # -- 69 | 70 | class NotImplemented < StandardError 71 | def initialize 72 | super "The feature is not implemented yet, sorry about that." 73 | end 74 | end 75 | 76 | # -- 77 | 78 | class RepoNotFound < StandardError 79 | def initialize(repo = nil) 80 | ending = repo ? "the repo '#{repo}'" : "your repo folder" 81 | super "Unable to find #{ 82 | ending 83 | }" 84 | end 85 | end 86 | 87 | # -- 88 | 89 | class ImageNotFound < StandardError 90 | def initialize(image) 91 | super "Unable to find the image #{ 92 | image 93 | }" 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/docker/template/logger.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | STDOUT.sync = true 5 | 6 | module Docker 7 | module Template 8 | class Logger 9 | class << self 10 | attr_writer :stdout, :stderr 11 | 12 | # -- 13 | 14 | def stdout 15 | return @stdout || $stdout 16 | end 17 | 18 | # -- 19 | 20 | def stderr 21 | return @stderr || $stderr 22 | end 23 | end 24 | 25 | # -- 26 | 27 | def initialize(repo = nil, stdout = nil, stderr = nil) 28 | @stdout = stdout || self.class.stdout 29 | @stderr = stderr || self.class.stderr 30 | @repo = repo 31 | @lines = { 32 | 0 => 0 33 | } 34 | end 35 | 36 | # -- 37 | 38 | def output? 39 | return !!@output 40 | end 41 | 42 | # -- 43 | 44 | def increment 45 | @lines.update({ 46 | @lines.size => @lines.size 47 | }) 48 | end 49 | 50 | # -- 51 | # A simple TTY stream that just prints out the data that it is given. 52 | # This is the logger that most will use for most of their building. 53 | # -- 54 | 55 | def tty(stream) 56 | @output = true 57 | @stdout.print(encode_str( 58 | stream 59 | )) 60 | end 61 | 62 | # -- 63 | # A simple logger that accepts a multi-type stream. 64 | # -- 65 | 66 | def simple(type, str) 67 | str = encode_str(str ||= "") 68 | type == :stderr ? @stderr.print(str) : \ 69 | @stdout.print(str) 70 | end 71 | 72 | # -- 73 | # A more complex streamer designed for the actual output of the Docker. 74 | # -- 75 | # This method will save parts into a buffer until it can either parse 76 | # that buffer or it parses the actual part itself, if it can parse the 77 | # part itself, it will first dump the buffer as errors and then parse. 78 | # -- 79 | # This method has to buffer because Docker-API (or Excon, it's depend) 80 | # gives us no indication of whether or not this is part of a larger chunk 81 | # it just dumps it on us, so we have to blindly work around that. 82 | # -- 83 | 84 | def api(part, *args) 85 | part = encode_str(part).each_line.to_a 86 | if part.one? && part = part.first 87 | chunked_part = @chunks.push(part).join if @chunks && !@chunks.empty? 88 | chunked_part = part if !@chunks 89 | stream = JSON.parse( 90 | chunked_part 91 | ) 92 | 93 | if chunked_part == part && @chunks && !@chunks.empty? 94 | then @chunks.each do |chunk| 95 | @stderr.puts format("Unparsable JSON: %s", 96 | chunk 97 | ) 98 | end 99 | end 100 | 101 | @chunks = nil 102 | return progress_bar(stream) if stream.any_key?("progress", "progressDetail") 103 | return output(stream["status"] || stream["stream"]) if stream.any_key?("status", "stream") 104 | return progress_error(stream) if stream.any_key?("errorDetail", "error") 105 | warn Simple::Ansi.red("Unhandled Stream.") 106 | @stdout.puts(part) 107 | @output = true 108 | else 109 | part.each do |v| 110 | api(v, *args) 111 | end 112 | end 113 | # -- 114 | # Sometimes we get undetectable chunks. 115 | # When we do, we try to keep them passed along. 116 | # That way we can throw them out later. 117 | # -- 118 | rescue JSON::ParserError => e 119 | (@chunks ||= []).push( 120 | part 121 | ) 122 | end 123 | 124 | # -- 125 | 126 | def output(msg) 127 | unless filter_matches?(msg) 128 | @stdout.puts msg 129 | increment 130 | end 131 | 132 | @output = true 133 | end 134 | 135 | # -- 136 | 137 | def progress_error(stream) 138 | abort Object::Simple::Ansi.red( 139 | stream["errorDetail"]["message"] 140 | ) 141 | end 142 | 143 | # -- 144 | # Some applications return some invalid ASCII so we need to work 145 | # around that so that no errors happen. This mostly happens 146 | # with Node.js NPM. 147 | # -- 148 | 149 | private 150 | def encode_str(str) 151 | str.encode("utf-8", { 152 | :invalid => :replace, :undef => :replace, :replace => "" 153 | }) 154 | end 155 | 156 | # -- 157 | 158 | private 159 | def progress_bar(stream) 160 | if ENV["CI"] != "true" 161 | id = stream["id"] 162 | 163 | return unless id 164 | before, diff = progress_diff(id) 165 | @stderr.print before if before 166 | str = stream["progress"] || stream["status"] 167 | str = "#{id}: #{str}\r" 168 | 169 | @stderr.print(Object::Simple::Ansi.jump( 170 | str, diff 171 | )) 172 | end 173 | end 174 | 175 | # -- 176 | 177 | private 178 | def progress_diff(id) 179 | if @lines.key?(id) 180 | return nil, @lines.size - @lines[id] 181 | end 182 | 183 | @lines[id] = @lines.size 184 | before = "\n" unless @lines.one? 185 | return before, 0 186 | end 187 | 188 | # -- 189 | 190 | private 191 | def filter_matches?(msg) 192 | return false unless @repo 193 | 194 | @repo.meta["log_filters"].any? do |filter| 195 | filter.is_a?(Regexp) && msg =~ filter || msg == filter 196 | end 197 | end 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/docker/template/meta.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "active_support/inflector" 6 | require "active_support/core_ext/hash/indifferent_access" 7 | require "yaml" 8 | 9 | module Docker 10 | module Template 11 | class Meta 12 | extend Forwardable::Extended 13 | attr_reader :data 14 | 15 | # -- 16 | # rubocop:disable Style/MultilineBlockLayout 17 | # -- 18 | 19 | [Pathutil.allowed[:yaml][:classes], Array.allowed[:keys], 20 | Hash.allowed[:vals]].each do |v| 21 | 22 | v.push(self, 23 | HashWithIndifferentAccess, Regexp 24 | ) 25 | end 26 | 27 | # -- 28 | # rubocop:enable Style/MultilineBlockLayout 29 | # -- 30 | 31 | DEFAULTS = HashWithIndifferentAccess.new({ 32 | "squash" => false, 33 | "startup" => true, 34 | "aliases" => {}, 35 | "build" => true, 36 | "cache" => false, 37 | "type" => "normal", 38 | "local_prefix" => "local", 39 | "project_data_dir" => "docker", 40 | "force" => ENV["CI"] == "true", 41 | "rootfs_base_img" => "envygeeks/alpine", 42 | "maintainer" => "Random User ", 43 | "envygeeks" => ENV["ENVYGEEKS"] && ENV["ENVYGEEKS"] == "true", 44 | "user" => ENV["USER"] || ENV["USERNAME"] || "random", 45 | "ci" => ENV["CI"] && ENV["CI"] == "true", 46 | "name" => Template.root.basename.to_s, 47 | "project_copy_dir" => "project", 48 | "rootfs_template" => "alpine", 49 | "push" => ENV["CI"] != "true", 50 | "cache_dir" => "cache", 51 | "repos_dir" => "repos", 52 | "copy_dir" => "copy", 53 | "tag" => "latest", 54 | "clean" => false, 55 | "tty" => false, 56 | "tags" => {}, 57 | 58 | # 59 | 60 | "log_filters" => [ 61 | /^The push refers to a repository/, 62 | /\sdigest: sha256:/ 63 | ], 64 | 65 | # 66 | 67 | "project_copy_ignore" => %w( 68 | .git 69 | .bundle 70 | Dockerfile 71 | vendor/bundle 72 | .gitattributes 73 | .node_modules 74 | .gitignore 75 | docker 76 | tmp 77 | log 78 | ), 79 | }).freeze 80 | 81 | # -- 82 | 83 | class << self 84 | def opts_file(force: nil) 85 | if force == :project || Template.project? 86 | then "docker/template.yml" else "opts.yml" 87 | end 88 | end 89 | end 90 | 91 | # -- 92 | # @param data [Hash, self.class] - the main data. 93 | # @param root [Hash, self.class] - the root data. 94 | # Create a new instance of `self.class`. 95 | # 96 | # @example ``` 97 | # self.class.new({ 98 | # :hello => :world 99 | # }) 100 | # ``` 101 | # -- 102 | # rubocop:disable Metrics/AbcSize 103 | # -- 104 | 105 | def initialize(overrides, root: nil) 106 | overrides = overrides.to_h :raw => true if overrides.is_a?(self.class) 107 | root = root.to_h :raw => true if root.is_a?(self.class) 108 | 109 | if root.nil? 110 | if Template.project? 111 | load_project_config( 112 | overrides 113 | ) 114 | 115 | else 116 | load_normal_config( 117 | overrides 118 | ) 119 | end 120 | 121 | @root = true 122 | else 123 | @data = overrides.stringify.with_indifferent_access 124 | @root_data = root.stringify.with_indifferent_access 125 | end 126 | 127 | debug! 128 | normalize! 129 | return 130 | end 131 | 132 | # -- 133 | 134 | def normalize! 135 | if root? 136 | opts = { 137 | :allowed_keys => [], 138 | :allowed_vals => [] 139 | } 140 | 141 | merge!({ 142 | "tags" => @data[ "tags"].stringify(**opts), 143 | "aliases" => @data["aliases"].stringify(**opts) 144 | }) 145 | end 146 | end 147 | 148 | # -- 149 | 150 | def debug! 151 | if root? && root_data["debug"] 152 | if !key?(:env) || self[:env].queryable? 153 | self[:env] ||= {} 154 | 155 | merge!({ 156 | :env => { 157 | :all => { 158 | :DEBUG => true 159 | } 160 | } 161 | }) 162 | end 163 | end 164 | end 165 | 166 | # -- 167 | 168 | def root_data 169 | return @root_data || @data 170 | end 171 | 172 | # -- 173 | 174 | def root 175 | if Template.project? 176 | then return Template.root.join(root_data[ 177 | :project_data_dir 178 | ]) 179 | 180 | else 181 | Template.root.join( 182 | root_data[:repos_dir], root_data[ 183 | :name 184 | ] 185 | ) 186 | end 187 | end 188 | 189 | # -- 190 | # Check if a part of the hash or a value is inside. 191 | # @param val [Anytning(), Hash] - The key or key => val you wish check. 192 | # @example meta.include?(:key => :val) => true|false 193 | # @example meta.include?(:key) => true|false 194 | # -- 195 | 196 | def include?(val) 197 | if val.is_a?(Hash) 198 | then val.stringify.each do |k, v| 199 | unless @data.key?(k) && @data[k] == v 200 | return false 201 | end 202 | end 203 | 204 | else 205 | return @data.include?( 206 | val 207 | ) 208 | end 209 | 210 | true 211 | end 212 | 213 | # -- 214 | # @param key [Anything()] the key you wish to pull. 215 | # @note we make the getter slightly more indifferent because of tags. 216 | # Pull an indifferent key from the hash. 217 | # -- 218 | 219 | def [](key) 220 | val = begin 221 | if key =~ /^\d+\.\d+$/ 222 | @data[key] || @data[ 223 | key.to_f 224 | ] 225 | 226 | elsif key =~ /^\d+$/ 227 | @data[key] || @data[ 228 | key.to_i 229 | ] 230 | 231 | else 232 | @data[key] 233 | end 234 | end 235 | 236 | if val.is_a?(Hash) 237 | return self.class.new(val, { 238 | :root => root_data 239 | }) 240 | end 241 | 242 | val 243 | end 244 | 245 | # -- 246 | 247 | def []=(key, val) 248 | hash = { key => val }.stringify 249 | @data.update( 250 | hash 251 | ) 252 | end 253 | 254 | # -- 255 | 256 | def update(hash) 257 | @data.update( 258 | hash.stringify 259 | ) 260 | end 261 | 262 | # -- 263 | 264 | def to_enum 265 | @data.each_with_object({}) do |(k, v), h| 266 | if v.is_a?(Hash) 267 | then v = self.class.new(v, { 268 | :root => root_data 269 | }) 270 | end 271 | 272 | h[k] = v 273 | end.to_enum 274 | end 275 | 276 | # -- 277 | # Merge a hash into the meta. If you merge non-queryable data 278 | # it will then get merged into the queryable data. 279 | # -- 280 | 281 | def merge(new_) 282 | if !queryable?(:query_data => new_) && queryable? 283 | new_ = { 284 | :all => new_ 285 | } 286 | end 287 | 288 | new_ = new_.stringify 289 | self.class.new(@data.deep_merge(new_), { 290 | :root => root_data 291 | }) 292 | end 293 | 294 | # -- 295 | # Destructive merging (@see self#merge) 296 | # -- 297 | 298 | def merge!(new_) 299 | if !queryable?(:query_data => new_) && queryable? 300 | new_ = { 301 | :all => new_ 302 | } 303 | end 304 | 305 | @data = @data.deep_merge( 306 | new_.stringify 307 | ) 308 | 309 | self 310 | end 311 | 312 | # -- 313 | # Check if a hash is queryable. AKA has "all", "group", "tag". 314 | # -- 315 | 316 | def queryable?(query_data: @data) 317 | if query_data.is_a?(self.class) 318 | then query_data 319 | .queryable? 320 | 321 | elsif !query_data || !query_data.is_a?(Hash) || query_data.empty? 322 | return false 323 | 324 | else 325 | (query_data.keys - %w( 326 | group tag all 327 | )).empty? 328 | end 329 | end 330 | 331 | # -- 332 | # Fallback, determining which route is the best. Tag > Group > All. 333 | # -- 334 | # rubocop:disable Metrics/CyclomaticComplexity 335 | # rubocop:disable Metrics/PerceivedComplexity 336 | # -- 337 | 338 | def fallback(group: current_group, tag: current_tag, query_data: @data) 339 | if query_data.is_a?(self.class) 340 | then query_data.fallback({ 341 | :group => group, :tag => tag 342 | }) 343 | 344 | elsif !query_data || !query_data.is_a?(Hash) || query_data.empty? 345 | return nil 346 | 347 | else 348 | if !(v = by_tag(:tag => tag, :query_data => query_data)).nil? then return v 349 | elsif !(v = by_parent_tag(:tag => tag, :query_data => query_data)).nil? then return v 350 | elsif !(v = by_group(:group => group, :query_data => query_data)).nil? then return v 351 | elsif !(v = by_parent_group(:tag => tag, :query_data => query_data)).nil? then return v 352 | else return for_all(:query_data => query_data) 353 | end 354 | end 355 | end 356 | 357 | # -- 358 | # rubocop:enable Metrics/CyclomaticComplexity 359 | # rubocop:enable Metrics/PerceivedComplexity 360 | # -- 361 | 362 | def for_all(query_data: @data) 363 | if query_data.is_a?(self.class) 364 | then query_data \ 365 | .for_all 366 | 367 | elsif !query_data || !query_data.is_a?(Hash) 368 | return nil 369 | 370 | else 371 | query_data.fetch( 372 | "all", nil 373 | ) 374 | end 375 | end 376 | 377 | # -- 378 | 379 | def by_tag(tag: current_tag, query_data: @data) 380 | if query_data.is_a?(self.class) 381 | then query_data.by_tag({ 382 | :tag => tag 383 | }) 384 | 385 | elsif !query_data || !query_data.is_a?(Hash) 386 | return nil 387 | 388 | else 389 | query_data.fetch("tag", {}).fetch( 390 | tag, nil 391 | ) 392 | end 393 | end 394 | 395 | # -- 396 | 397 | def by_parent_tag(tag: current_tag, query_data: @data) 398 | if aliased_tag == current_tag || !complex_alias? 399 | return nil 400 | 401 | else 402 | by_tag({ 403 | :query_data => query_data, 404 | :tag => aliased_tag({ 405 | :tag => tag 406 | }) 407 | }) 408 | end 409 | end 410 | 411 | # -- 412 | 413 | def by_group(group: current_group, query_data: @data) 414 | if query_data.is_a?(self.class) 415 | then query_data.by_group({ 416 | :group => group 417 | }) 418 | 419 | elsif !query_data || !query_data.is_a?(Hash) 420 | return nil 421 | 422 | else 423 | query_data.fetch("group", {}).fetch( 424 | group, nil 425 | ) 426 | end 427 | end 428 | 429 | # -- 430 | 431 | def by_parent_group(tag: current_tag, query_data: @data) 432 | if aliased_tag == current_tag || !complex_alias? 433 | return nil 434 | 435 | else 436 | by_group({ 437 | :query_data => query_data, 438 | :group => aliased_group({ 439 | :tag => tag 440 | }) 441 | }) 442 | end 443 | end 444 | 445 | # -- 446 | # Checks to see if the current meta is an alias of another. This 447 | # happens when the user has the tag in aliases but it's not complex. 448 | # -- 449 | 450 | def alias? 451 | !!(aliased_tag && aliased_tag != tag) 452 | end 453 | 454 | # -- 455 | # A complex alias happens when the user has an alias but also tries to 456 | # add extra data, this allows them to use data from all parties. This 457 | # allows them to reap the benefits of having shared data but sometimes 458 | # independent data that diverges into it's own. 459 | # -- 460 | 461 | def complex_alias? 462 | if !alias? 463 | return false 464 | 465 | else 466 | !!root_data.find do |_, v| 467 | (v.is_a?(self.class) || v.is_a?(Hash)) && queryable?(:query_data => v) \ 468 | && by_tag(:query_data => v) 469 | end 470 | end 471 | end 472 | 473 | # -- 474 | 475 | def aliased_tag(tag: current_tag) 476 | aliases = root_data[:aliases] 477 | if aliases.nil? || !aliases.key?(tag) 478 | tag 479 | 480 | else 481 | aliases[ 482 | tag 483 | ] 484 | end 485 | end 486 | 487 | # -- 488 | 489 | def aliased_group(tag: current_tag) 490 | root_data[:tags][aliased_tag({ 491 | :tag => tag 492 | })] 493 | end 494 | 495 | # -- 496 | # Converts the current meta into a string. 497 | # -- 498 | 499 | def to_s(raw: false, shell: false) 500 | if !raw && (mergeable_hash? || mergeable_array?) 501 | to_a(:shell => shell).join(" #{ 502 | "\n" if shell 503 | }") 504 | 505 | elsif !raw && queryable? 506 | then fallback \ 507 | .to_s 508 | 509 | else 510 | @data.to_s 511 | end 512 | end 513 | 514 | # -- 515 | # rubocop:disable Metrics/CyclomaticComplexity 516 | # rubocop:disable Metrics/PerceivedComplexity 517 | # -- 518 | 519 | def to_a(raw: false, shell: false) 520 | if raw 521 | return to_h({ 522 | :raw => true 523 | }).to_a 524 | 525 | elsif !mergeable_array? 526 | to_h.each_with_object([]) do |(k, v), a| 527 | a << "#{k}=#{ 528 | shell ? v.to_s.shellescape : v 529 | }" 530 | end 531 | else 532 | (for_all || []) | (by_parent_group || []) | (by_group || []) | \ 533 | (by_parent_tag || []) | (by_tag || []) 534 | end 535 | end 536 | 537 | # -- 538 | # rubocop:eanble Metrics/CyclomaticComplexity 539 | # rubocop:eanble Metrics/PerceivedComplexity 540 | # -- 541 | # Convert a `Meta' into a normal hash. If `self' is queryable then 542 | # we go and start merging values smartly. This means that we will merge 543 | # all the arrays into one another and we will merge hashes into hashes. 544 | # -- 545 | # rubocop:disable Metrics/AbcSize 546 | # -- 547 | 548 | def to_h(raw: false) 549 | return @data.to_h if raw || !queryable? || !mergeable_hash? 550 | keys = [for_all, by_group, by_parent_group, by_tag, \ 551 | by_parent_tag].compact.map(&:keys) 552 | 553 | keys.reduce(:+).each_with_object({}) do |k, h| 554 | vals = [for_all, by_group, by_parent_group, by_tag, \ 555 | by_parent_tag].compact 556 | 557 | h[k] = \ 558 | if mergeable_array?(k) 559 | vals.map { |v| v[k].to_a } \ 560 | .compact.reduce( 561 | :+ 562 | ) 563 | 564 | elsif mergeable_hash?(k) 565 | vals.map { |v| v[k].to_h } \ 566 | .compact.reduce( 567 | :deep_merge 568 | ) 569 | 570 | else 571 | vals.find do |v| 572 | v.key?( 573 | k 574 | ) 575 | end \ 576 | [k] 577 | end 578 | end 579 | end 580 | 581 | # -- 582 | # rubocop:enable Metrics/AbcSize 583 | # -- 584 | 585 | def mergeable_hash?(key = nil) 586 | return false unless queryable? 587 | vals = [by_parent_tag, by_parent_group, \ 588 | by_tag, for_all, by_group].compact 589 | 590 | if key 591 | vals = vals.map do |val| 592 | val[key] 593 | end 594 | end 595 | 596 | !vals.empty? && !vals.any? do |val| 597 | !val.is_a?(Hash) && !val.is_a?( 598 | self.class 599 | ) 600 | end 601 | end 602 | 603 | # -- 604 | 605 | def mergeable_array?(key = nil) 606 | return false unless queryable? 607 | vals = [by_parent_tag, by_parent_group, \ 608 | by_tag, for_all, by_group].compact 609 | 610 | if key 611 | vals = vals.map do |val| 612 | val[key] 613 | end 614 | end 615 | 616 | !vals.empty? && !vals.any? do |val| 617 | !val.is_a?( 618 | Array 619 | ) 620 | end 621 | end 622 | 623 | # -- 624 | 625 | def current_group 626 | root_data[:tags][current_tag] || 627 | "normal" 628 | end 629 | 630 | # -- 631 | # HELPER: Get a list of all the tags. 632 | # -- 633 | 634 | def tags 635 | (root_data[:tags] || {}).keys | (root_data[:aliases] || {}).keys 636 | end 637 | 638 | # -- 639 | # HELPER: Get a list of all the groups. 640 | # -- 641 | 642 | def groups 643 | root_data["tags"].values.uniq 644 | end 645 | 646 | # -- 647 | 648 | private 649 | def merge_or_override(val, new_val) 650 | return new_val unless val 651 | return val if val.is_a?(String) && !new_val || !new_val.is_a?(val.class) 652 | return new_val.merge(val) if val.respond_to?(:merge) 653 | return new_val | val if val.respond_to?(:|) 654 | end 655 | 656 | # -- 657 | 658 | private 659 | def string_wrapper(obj, shell: false) 660 | return obj if obj == true || obj == false || obj.nil? 661 | return obj.fallback if obj.is_a?(self.class) && obj.queryable? \ 662 | && !(o = obj.fallback).nil? && (o == true || o == false) 663 | 664 | return obj.to_s(:shell => shell) if obj.is_a?(self.class) 665 | !obj.is_a?(Array) ? obj.to_s : obj.join( 666 | "\s" 667 | ) 668 | end 669 | 670 | # -- 671 | 672 | private 673 | def method_missing(method, *args, shell: false, &block) 674 | key = method.to_s.gsub(/\?$/, "") 675 | val = self[key] || self[key.singularize] \ 676 | || self[key.pluralize] 677 | 678 | if !args.empty? || block_given? 679 | super 680 | 681 | elsif method !~ /\?$/ 682 | string_wrapper(val, { 683 | :shell => shell 684 | }) 685 | 686 | else 687 | val = val.fallback if val.is_a?(self.class) && val.queryable? 688 | [true, false].include?(val) ? val : \ 689 | if val.respond_to?(:empty?) 690 | then !val.empty? else !!val 691 | end 692 | end 693 | end 694 | 695 | # -- 696 | 697 | private 698 | def load_normal_config(overrides) 699 | overrides = overrides.stringify 700 | gdata = Template.root.join(self.class.opts_file).read_yaml 701 | @data = DEFAULTS.deep_merge(gdata.stringify).deep_merge(overrides) 702 | tdata = Template.root.join(@data[:repos_dir], @data[:name], self.class.opts_file).read_yaml 703 | @data = @data.deep_merge(tdata.stringify).deep_merge(overrides) 704 | @data = @data.stringify.with_indifferent_access 705 | end 706 | 707 | # -- 708 | 709 | private 710 | def load_project_config(overrides) 711 | overrides = overrides.stringify 712 | gdata = Template.root.join(self.class.opts_file).read_yaml 713 | @data = DEFAULTS.deep_merge(gdata.stringify).deep_merge(overrides) 714 | @data = @data.stringify.with_indifferent_access 715 | end 716 | 717 | # -- 718 | 719 | alias deep_merge merge 720 | alias group current_group 721 | rb_delegate :for_all, :to => :self, :type => :hash, :key => :all 722 | rb_delegate :current_tag, :to => :root_data, :key => :tag, :type => :hash 723 | rb_delegate :tag, :to => :root_data, :type => :hash, :key => :tag 724 | rb_delegate :root, :to => :@root, :type => :ivar, :bool => true 725 | 726 | # -- 727 | 728 | rb_delegate :fetch, :to => :@data 729 | rb_delegate :delete, :to => :@data 730 | rb_delegate :empty?, :to => :@data 731 | rb_delegate :inspect, :to => :@data 732 | rb_delegate :values_at, :to => :@data 733 | rb_delegate :values, :to => :@data 734 | rb_delegate :keys, :to => :@data 735 | rb_delegate :key?, :to => :@data 736 | rb_delegate :==, :to => :@data 737 | 738 | # -- 739 | 740 | rb_delegate :inject, :to => :to_enum 741 | rb_delegate :select, :to => :to_enum 742 | rb_delegate :each_with_object, :to => :to_enum 743 | rb_delegate :collect, :to => :to_enum 744 | rb_delegate :find, :to => :to_enum 745 | rb_delegate :each, :to => :to_enum 746 | end 747 | end 748 | end 749 | -------------------------------------------------------------------------------- /lib/docker/template/notify.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | module Notify 8 | module_function 9 | 10 | # -- 11 | # Notify the user of a push that is happening. 12 | # -- 13 | 14 | def push(builder) 15 | $stderr.puts Simple::Ansi.green( 16 | "Pushing: #{builder.repo}" 17 | ) 18 | end 19 | 20 | # -- 21 | # Notify the user that we are tag aliasing. 22 | # -- 23 | 24 | def alias(builder) 25 | repo = builder.repo 26 | aliased_repo = builder.aliased_repo || builder.aliased_tag 27 | msg = Simple::Ansi.green("Aliasing #{repo} -> #{aliased_repo}") 28 | $stderr.puts msg 29 | end 30 | 31 | # -- 32 | 33 | def build(repo, rootfs: false) 34 | build_start(repo, { 35 | :rootfs => rootfs 36 | }) 37 | 38 | if block_given? 39 | yield 40 | build_end(repo, { 41 | :rootfs => rootfs 42 | }) 43 | end 44 | end 45 | 46 | # -- 47 | # Notify the user that we are building their repository. 48 | # -- 49 | 50 | def build_start(repo, rootfs: false) 51 | if ENV["TRAVIS"] && !ENV.key?("RSPEC_RUNNING") 52 | STDOUT.puts(format("travis_fold:end:%s", 53 | repo.to_s(:rootfs => rootfs).tr("^A-Za-z0-9", "-").gsub( 54 | /\-$/, "" 55 | ) 56 | )) 57 | end 58 | 59 | $stderr.puts Simple::Ansi.green(format( 60 | "Building: %s", repo.to_s({ 61 | :rootfs => rootfs 62 | }) 63 | )) 64 | end 65 | 66 | # -- 67 | # Notify the user that building their repository has ended. 68 | # -- 69 | 70 | def build_end(repo, rootfs: false) 71 | if ENV["TRAVIS"] && !ENV.key?("RSPEC_RUNNING") 72 | STDOUT.puts(format("travis_fold:end:%s", 73 | repo.to_s(:rootfs => rootfs).tr("^A-Za-z0-9", "-").gsub( 74 | /\-$/, "" 75 | ) 76 | )) 77 | end 78 | 79 | $stderr.puts Simple::Ansi.green(format( 80 | "Done Building: %s", repo.to_s({ 81 | :rootfs => rootfs 82 | }) 83 | )) 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/docker/template/parser.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class Parser 8 | SLASH_REGEXP = /\// 9 | SPLIT_REGEXP = /:|\// 10 | COLON_REGEXP = /:/ 11 | 12 | def initialize(raw_repos = [], argv = {}) 13 | @raw_repos = raw_repos 14 | @argv = argv 15 | end 16 | 17 | # -- 18 | # Return `raw_repos` if you send us a list of repos you wish to build, 19 | # otherwise we get the children of the repo folder and ship that off so 20 | # you can build *every* repo, I don't know if you want that. 21 | # -- 22 | def all 23 | return @raw_repos unless @raw_repos.empty? 24 | return [Template.root.basename.to_s] if Template.project? 25 | Template.root.join(Meta.new({}).repos_dir).children.map do |path| 26 | path.basename.to_s 27 | end 28 | 29 | rescue Errno::ENOENT 30 | then raise( 31 | Error::RepoNotFound 32 | ) 33 | end 34 | 35 | # -- 36 | # rubocop:disable Metrics/AbcSize 37 | # -- 38 | def parse 39 | scratch = [] 40 | simple = [] 41 | aliases = [] 42 | 43 | all.each do |v| 44 | hash = self.class.to_repo_hash(v) 45 | raise Error::BadRepoName, v if hash.empty? 46 | Repo.new(hash, @argv).to_repos.each do |r| 47 | scratch << r if r.builder.scratch? && !r.alias? 48 | simple << r unless r.alias? || r.builder.scratch? 49 | aliases << r if r.alias? 50 | end 51 | end 52 | 53 | out = aliases.each_with_object(scratch | simple) do |alias_, repos| 54 | index = repos.rindex { |v| v.name == alias_.name } 55 | if index 56 | repos.insert(index + 1, 57 | alias_ 58 | ) 59 | 60 | else 61 | repos.push( 62 | alias_ 63 | ) 64 | end 65 | end 66 | end 67 | 68 | # -- 69 | # rubocop:enable Metrics/AbcSize 70 | # -- 71 | def self.to_repo_hash(val) 72 | data = val.to_s.split(SPLIT_REGEXP) 73 | 74 | return "name" => data[0] if data.one? 75 | return "name" => data[0], "tag" => data[1] if val =~ COLON_REGEXP && data.size == 2 76 | return "user" => data[0], "name" => data[1] if val =~ SLASH_REGEXP && data.size == 2 77 | return "user" => data[0], "name" => data[1], "tag" => data[2] if data.size == 3 78 | 79 | {} 80 | end 81 | 82 | # -- 83 | 84 | def self.from_tag_to_repo_hash(val) 85 | data = val.to_s.split(SPLIT_REGEXP) 86 | 87 | return "tag" => data[0] if data.one? 88 | return "name" => data[0], "tag" => data[1] if val =~ COLON_REGEXP && data.size == 2 89 | return "user" => data[0], "name" => data[1] if val =~ SLASH_REGEXP && data.size == 2 90 | return "user" => data[0], "name" => data[1], "tag" => data[2] if data.size == 3 91 | 92 | {} 93 | end 94 | 95 | # -- 96 | 97 | def self.full_name?(val) 98 | parsed = to_repo_hash(val) 99 | parsed.key?("name") && (parsed.key?("user") || parsed.key?( 100 | "tag" 101 | )) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/docker/template/repo.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | class Repo 8 | extend Forwardable::Extended 9 | 10 | # -- 11 | 12 | def initialize(*hashes) 13 | @base_meta = hashes.compact 14 | @base_meta = @base_meta.reduce(:deep_merge) 15 | @base_meta.freeze 16 | 17 | unless root.exist? 18 | raise( 19 | Error::RepoNotFound, name 20 | ) 21 | end 22 | end 23 | 24 | # -- 25 | 26 | def pushable? 27 | (meta["push"] || meta["push_only"]) && 28 | !meta["cache_only"] && !meta[ 29 | "clean_only" 30 | ] 31 | end 32 | 33 | # -- 34 | 35 | def cacheable? 36 | (meta["cache"] || meta["cache_only"]) && 37 | !meta[ 38 | "push_only" 39 | ] 40 | end 41 | 42 | # -- 43 | 44 | def clean_cache? 45 | (meta["clean"] || meta["clean_only"]) 46 | end 47 | 48 | # -- 49 | 50 | def buildable? 51 | meta.build? && !meta["push_only"] && !meta["cache_only"] && 52 | !meta[ 53 | "clean_only" 54 | ] 55 | end 56 | 57 | # -- 58 | # Pulls out the repo this repo is aliasing it, this happens when you 59 | # when you set the tag in the "alias" section of your `opts.yml`. 60 | # -- 61 | def aliased 62 | full = Parser.full_name?( 63 | meta.aliased_tag 64 | ) 65 | 66 | if alias? && full 67 | self.class.new(to_h.merge(Parser.to_repo_hash( 68 | meta.aliased_tag 69 | ))) 70 | 71 | elsif alias? 72 | self.class.new(to_h.merge({ 73 | "tag" => meta.aliased_tag 74 | })) 75 | end 76 | 77 | rescue Error::RepoNotFound => e 78 | unless full 79 | raise e 80 | end 81 | end 82 | 83 | # -- 84 | # Initializes and returns the builder so that you can build the repo. 85 | # -- 86 | def builder 87 | return @builder ||= begin 88 | Template::Builder.const_get(type.capitalize).new( 89 | self 90 | ) 91 | end 92 | end 93 | 94 | # -- 95 | # Convert the repo into it's final image name, however if you tell, us 96 | # this is a rootfs build we will convert it into the rootfs name. 97 | # -- 98 | def to_s(rootfs: false) 99 | prefix = meta["local_prefix"] 100 | return "#{user}/#{name}:#{tag}" unless rootfs 101 | "#{prefix}/rootfs:#{name}" 102 | end 103 | 104 | # -- 105 | # The directory you wish to cache to (like `cache/`) or other. 106 | # -- 107 | def cache_dir 108 | return root.join( 109 | meta["cache_dir"], tag 110 | ) 111 | end 112 | 113 | # -- 114 | # The directory you store your image data in (by default `copy/`.) 115 | # -- 116 | def copy_dir(*path) 117 | dir = meta["copy_dir"] 118 | root.join( 119 | dir, *path 120 | ) 121 | end 122 | 123 | # -- 124 | 125 | def to_tag_h 126 | { 127 | "tag" => tag, 128 | "repo" => "#{user}/#{name}", 129 | "force" => true 130 | } 131 | end 132 | 133 | # -- 134 | 135 | def to_rootfs_h 136 | { 137 | "tag" => name, 138 | "repo" => "#{meta["local_prefix"]}/rootfs", 139 | "force" => true 140 | } 141 | end 142 | 143 | # -- 144 | 145 | def tmpdir(*args, root: Template.tmpdir) 146 | args.unshift(user.gsub(/[^A-Za-z0-9_\-]+/, "--"), name, tag) 147 | out = Pathutil.tmpdir(args, nil, root) 148 | out.realpath 149 | end 150 | 151 | # -- 152 | 153 | def tmpfile(*args, root: Template.tmpdir) 154 | args.unshift(user, name, tag) 155 | Pathutil.tmpfile( 156 | args, nil, root 157 | ) 158 | end 159 | 160 | # -- 161 | # If a tag was given then it returns [self] and if a tag was not sent 162 | # it then goes on to detect the type and split itself accordingly 163 | # returning multiple, AKA all repos that should be built. 164 | # -- 165 | def to_repos 166 | if Template.project? 167 | then Set.new([ 168 | self 169 | ]) 170 | 171 | else 172 | set = Set.new 173 | if @base_meta.key?("tag") 174 | set << self 175 | else 176 | tags.each do |tag| 177 | hash = Parser.from_tag_to_repo_hash(tag) 178 | hash = to_h.merge(hash) 179 | set << self.class.new( 180 | hash, @cli_opts 181 | ) 182 | end 183 | end 184 | 185 | set 186 | end 187 | end 188 | 189 | # -- 190 | 191 | def meta 192 | return @meta ||= begin 193 | Meta.new( 194 | @base_meta 195 | ) 196 | end 197 | end 198 | 199 | # -- 200 | 201 | def to_env(tar_gz: nil, copy_dir: nil) 202 | hash = meta["env"] || { "all" => {}} 203 | Meta.new(hash, :root => meta).merge({ 204 | "REPO" => name, 205 | "TAR_GZ" => tar_gz, 206 | "GROUP" => meta.group, 207 | "DEBUG" => meta.debug?? "true" : "", 208 | "COPY_DIR" => copy_dir, 209 | "BUILD_TYPE" => type, 210 | "TAG" => tag 211 | }) 212 | end 213 | 214 | # -- 215 | 216 | rb_delegate :build, :to => :builder 217 | rb_delegate :alias?, :to => :meta 218 | rb_delegate :complex_alias?, :to => :meta 219 | rb_delegate :type, :to => :meta, :type => :hash 220 | rb_delegate :user, :to => :meta, :type => :hash 221 | rb_delegate :name, :to => :meta, :type => :hash 222 | rb_delegate :tag, :to => :meta, :type => :hash 223 | rb_delegate :to_h, :to => :@base_meta 224 | rb_delegate :root, :to => :meta 225 | rb_delegate :tags, :to => :meta 226 | rb_delegate :clean, { 227 | :to => Cache, :alias_of => :cleanup, :args => %w( 228 | self 229 | ) 230 | } 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /lib/docker/template/version.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Docker 6 | module Template 7 | VERSION = "0.22.0" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/erb/context.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "erb" 6 | class ERB 7 | class Context 8 | 9 | # -- 10 | # Wraps any data you wish to send to ERB limiting it's context access. 11 | # @param [Hash] vars the variables you wish to set 12 | # -- 13 | def initialize(vars) 14 | vars.each do |key, val| 15 | instance_variable_set("@#{key}", val) 16 | end 17 | end 18 | 19 | # -- 20 | # Returns the binding so that we can ship it off and give it to ERB. 21 | # -- 22 | def _binding 23 | return binding 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /script/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: 2017 - 2018 - MIT License 3 | # Source: https://github.com/envygeeks/devfiles 4 | # Author: Jordon Bedwell 5 | [ "$DEBUG" = "true" ] && set -x 6 | set -e 7 | 8 | f=script/script.d/install 9 | [ "$SKIP_SCRIPTD" != "true" ] && [ -x $f ] && exec $f "$@" 10 | if bundle check 1>/dev/null 2>&1; then 11 | bundle update 12 | else 13 | bundle install --path \ 14 | vendor/bundle 15 | fi 16 | 17 | bundle clean 18 | if [ -f package.json ]; then 19 | yarn 20 | fi 21 | -------------------------------------------------------------------------------- /script/rake.d/update.rake: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | $LOAD_PATH.unshift(File.expand_path( 6 | "../lib", __FILE__ 7 | )) 8 | 9 | # -- 10 | 11 | require "simple/ansi" 12 | require "docker/template/cli" 13 | require "open3" 14 | 15 | # -- 16 | # TODO: Cleanup and remove this whenever you can. 17 | # -- 18 | module CompList 19 | module_function 20 | 21 | # -- 22 | # Update the pak file to have all the completions. 23 | # -- 24 | def update(data = get_commands, msgp = data.to_msgpack) 25 | pak_file.binwrite( 26 | msgp 27 | ) 28 | end 29 | 30 | # -- 31 | 32 | def normalize_command(command) 33 | if command.is_a?(Array) 34 | then command.map do |key| 35 | key.tr( 36 | "_", "-" 37 | ) 38 | end 39 | else 40 | command.tr( 41 | "_", "-" 42 | ) 43 | end 44 | end 45 | 46 | # -- 47 | # Provides the base "_reply" for your auto-complete data output. 48 | # -- 49 | def base(const, skip = %w(help)) 50 | keys = const.all_commands.keys 51 | return "_reply" => normalize_command(keys), "help" => { 52 | "_reply" => normalize_command(keys) - skip 53 | } 54 | end 55 | 56 | # -- 57 | 58 | def add_opts(out, const) 59 | const.all_commands.each do |key, val, command = normalize_command(key)| 60 | val.options.map do |_, opt| 61 | out[command] ||= { "_reply" => [] } 62 | ary = out[command][ 63 | "_reply" 64 | ] 65 | 66 | if !opt.boolean? 67 | ary << "#{ 68 | opt.switch_name 69 | }=" 70 | 71 | else 72 | ary << opt.switch_name 73 | ary << "--no-#{opt.switch_name.gsub( 74 | /\A--/, "" 75 | )}" 76 | end 77 | 78 | ary | opt.aliases 79 | end 80 | end 81 | 82 | out 83 | end 84 | 85 | # -- 86 | # Recursively pulls out and set's up your commands and opts. 87 | # -- 88 | def get_commands(const = Docker::Template::CLI) 89 | out = base( 90 | const 91 | ) 92 | 93 | const.subcommands.each do |key, command = normalize_command(key)| 94 | const_list = const.to_namespace.push(command.to_namespace) 95 | out[command] = send(__method__, Thor::Namespace.resolv( 96 | const_list 97 | )) 98 | end 99 | 100 | add_opts( 101 | out, const 102 | ) 103 | end 104 | 105 | # -- 106 | 107 | def pak_file 108 | Pathutil.new("comp/list.pak").expand_path.tap( 109 | &:touch 110 | ) 111 | end 112 | end 113 | 114 | # -- 115 | 116 | namespace :update do 117 | desc "Update the completion list." 118 | task "comp-list" do 119 | require "msgpack" 120 | require "docker/template" 121 | CompList.update 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /script/report: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: 2017 - 2018 - MIT License 3 | # Source: https://github.com/envygeeks/devfiles 4 | # Author: Jordon Bedwell 5 | [ "$DEBUG" = "true" ] && set -x 6 | set -e 7 | 8 | f=script/script.d/report 9 | [ "$SKIP_SCRIPTD" != "true" ] && [ -x $f ] && exec $f "$@" 10 | if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS" = "true" ]; then 11 | if [ "$1" = "before" ]; then 12 | # TODO: Start tracking the SHA sum, this is crazy.. 13 | url=https://codeclimate.com/downloads/test-reporter 14 | url=$url/test-reporter-latest-linux-amd64 15 | curl -sL $url > ./cctr 16 | 17 | # -- 18 | # Eventually they should make this the default on Travis-CI 19 | # this way I don't worry about some strange stuff going on with 20 | # a possible compromise, and an invalid SHA. 21 | # -- 22 | 23 | chmod +x ./cctr && ./cctr before-build 24 | else 25 | ./cctr after-build --exit-code \ 26 | $TRAVIS_TEST_RESULT 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /script/sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: 2017 - 2018 - MIT License 3 | # Source: https://github.com/envygeeks/devfiles 4 | # Author: Jordon Bedwell 5 | # -- 6 | 7 | [ "$DEBUG" = "true" ] && set -x 8 | set -e 9 | 10 | # -- 11 | trim() { 12 | echo "$@" | tr -s " " | \ 13 | sed -e 's/^[[:space:]]*//g' | \ 14 | sed -e 's/[[:space:]]$//g' 15 | } 16 | 17 | # -- 18 | [ -n "$1" ] && git config envygeeks.language "$1" 19 | [ -n "$2" ] && git config envygeeks.external "$2" 20 | 21 | # -- 22 | # Determine what we are working with. 23 | # -- 24 | 25 | skip_if_exists=(Gemfile .travis.yml) 26 | sync_language=$(git config envygeeks.language || echo "") 27 | skip_if_external=(CONTRIBUTING.md CODE_OF_CONDUCT.md .github/codeowners) 28 | skip_if_external+=(.github/issue_template.md .github/pull_request_template.md) 29 | external=$(git config envygeeks.external || echo "false") 30 | github_url=https://github.com/envygeeks/devfiles.git 31 | copy_to=$PWD 32 | 33 | # -- 34 | # Copies everything, except for what's to be skipped if 35 | # it happens to exist, into the current directory, so that 36 | # stays in sync with the parent. 37 | # -- 38 | 39 | clone_dir=$(mktemp -d) 40 | git clone $github_url "$clone_dir" 41 | for d in $(trim global $sync_language); do 42 | cd "$clone_dir" 43 | 44 | [ ! -d "$d" ] && continue 45 | find "$d" -not -path "$d" -type f | while read f; do 46 | to=$(echo "$f" | sed -e "s/$d\///"); dir=$(dirname "$to") 47 | if [ "$dir" != "$d" ] && [ "$dir" != "." ]; then 48 | if [ ! -d "$copy_to/$dir" ]; then 49 | bash -xc "mkdir -p '$copy_to/$dir'" 50 | fi 51 | fi 52 | 53 | skip=false 54 | for sf in "${skip_if_exists[@]}"; do 55 | if [ "$to" = "$sf" ] && [ -f "$copy_to/$sf" ]; then 56 | skip=true 57 | fi 58 | done 59 | 60 | if [ "$skip" = "false" ] && [ "$external" = "true" ]; then 61 | for sf in "${skip_if_external[@]}"; do 62 | if [ "$to" = "$sf" ] && [ -f "$copy_to/$sf" ]; then 63 | skip=true 64 | fi 65 | done 66 | fi 67 | 68 | if [ "$skip" != "true" ]; then 69 | bash -xc "cp -L '$f' '$copy_to/$to'" 70 | fi 71 | done 72 | done 73 | 74 | rm -rf $clone_dir 75 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: 2017 - 2018 - MIT License 3 | # Source: https://github.com/envygeeks/devfiles 4 | # Author: Jordon Bedwell 5 | [ "$DEBUG" = "true" ] && set -x 6 | set -e 7 | 8 | # -- 9 | hasGem() { 10 | if cat Gemfile | grep -qEi "^\s*gem\s+(\"|')$1(\\1)"; then 11 | return 0 12 | else 13 | find . -maxdepth 1 -type f -name \*.gemspec | while read f; do 14 | if cat "$f" | grep -qEi "(\"|')$1(\\1)"; then 15 | return 1 16 | fi 17 | done && return 1 || return 0 18 | fi 19 | 20 | return 1 21 | } 22 | 23 | # -- 24 | # This logic exists so that on CI's all you need do is 25 | # set an environment variable to kick on linting, without 26 | # having to do excessive customization of the CI. 27 | # -- 28 | f=script/script.d/test 29 | [ "$SKIP_SCRIPTD" != "true" ] && [ -x $f ] && exec $f "$@" 30 | hasGem "minitest" && [ $# -gt 0 ] && exec ruby -rminitest/autorun "$@" 31 | hasGem "minitest" && exec ruby -rminitest/autorun test/**/*_{test,spec}.rb 32 | hasGem "rspec" && exec bundle exec rspec "$@" 33 | hasGem "rspec-rails" && exec bundle exec rspec "$@" 34 | [ -x bin/rails ] && exec bin/rails test "$@" 35 | [ $# -gt 0 ] && exec ruby -Itest "$@" 36 | exec ruby -Itest test/**/*_test.rb 37 | -------------------------------------------------------------------------------- /spec/rspec/helper.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | ENV.delete("CI") 6 | require "support/coverage" 7 | require "luna/rspec/formatters/checks" 8 | require "docker/template" 9 | require "rspec/helpers" 10 | 11 | ENV["RSPEC_RUNNING"] ||= "true" 12 | Dir[File.expand_path("../../support/**/*.rb", __FILE__)].each do |f| 13 | require f 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/clear.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | RSpec.configure do |config| 6 | config.before do |ex| 7 | unless ex.metadata[:clear] 8 | allow(Simple::Ansi).to receive(:clear).and_return( 9 | nil 10 | ) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/color.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | RSpec.configure do |config| 6 | config.color = true 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/contexts/docker.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | shared_context :docker do 6 | before do 7 | allow(Docker::Container).to receive(:create) do 8 | container_mock 9 | end 10 | end 11 | 12 | # 13 | 14 | let :container_mock do 15 | Mocks::Container \ 16 | .new 17 | end 18 | 19 | # 20 | 21 | before do 22 | allow(Docker::Image).to receive(:build_from_dir) do 23 | image_mock 24 | end 25 | end 26 | 27 | # 28 | 29 | let :image_mock do 30 | Mocks::Image \ 31 | .new 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/contexts/repos.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | shared_context :repos do 6 | before do |ex| 7 | mocked_repo.setup 8 | unless ex.metadata[:init] == false 9 | mocked_repo.init({ 10 | :type => ex.metadata[:type] ||= :scratch 11 | }) 12 | end 13 | end 14 | 15 | # 16 | 17 | let :mocked_repo do 18 | Mocks::Repo.new( 19 | self 20 | ) 21 | end 22 | 23 | # 24 | 25 | after do 26 | mocked_repo.teardown 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "simplecov" 6 | SimpleCov \ 7 | .start 8 | -------------------------------------------------------------------------------- /spec/support/filter.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | RSpec.configure do |config| 6 | config.filter_run_excluding :ruby => (lambda do |type| 7 | RUBY_ENGINE == begin 8 | type == "!mri" || type == "!ruby" ? "ruby" : \ 9 | if type == "!jruby" 10 | "jruby" 11 | end 12 | end 13 | end) 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/matchers/have_const.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | RSpec::Matchers.define :have_const do |const| 6 | match do |owner| 7 | owner.const_defined?(const) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/mocks/container.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Mocks 6 | class Container 7 | attr_reader :struct 8 | def initialize 9 | @mocked = [ 10 | :delete, 11 | :streaming_logs, 12 | :attach, 13 | :stop, 14 | :logs 15 | ] 16 | end 17 | 18 | # -- 19 | 20 | def json 21 | { 22 | "State" => { 23 | "ExitCode" => 0 24 | } 25 | } 26 | end 27 | 28 | # -- 29 | 30 | def start(*_) 31 | self 32 | end 33 | 34 | # -- 35 | 36 | def method_missing(method, *args, &block) 37 | if @mocked.include?(method) 38 | nil else super 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/mocks/image.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | module Mocks 6 | class Image 7 | def tag(*args, &block); end 8 | def delete(*args, &block); end 9 | def push(*args, &block); end 10 | def id(*args, &block); end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/mocks/repo.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "yaml" 6 | 7 | module Mocks 8 | class Repo 9 | extend Forwardable::Extended 10 | 11 | FS_LAYOUTS = { 12 | :project => [ 13 | [:mkdir, "docker"], 14 | [:write, "docker/copy/all/usr/local/bin/hello", "world"], 15 | [:write, "docker/template.yml", "--- { force: false, push: false }\n"], 16 | [:touch, "Dockerfile"] 17 | ], 18 | 19 | :normal => [ 20 | [:mkdir, "../../copy"], 21 | [:mkdir, "copy"], 22 | [:mkdir, "copy/all"], 23 | [:mkdir, "copy/tag/latest"], 24 | [:mkdir, "copy/group/normal"], 25 | [:write, "copy/all/usr/local/bin/hello", "world"], 26 | [:touch, "Dockerfile"], 27 | [:with_opts, { 28 | :push => false, 29 | :force => false, 30 | }] 31 | ], 32 | 33 | :scratch => [ 34 | [:mkdir, "../../copy"], 35 | [:mkdir, "copy"], 36 | [:mkdir, "copy/all"], 37 | [:mkdir, "copy/tag/latest"], 38 | [:mkdir, "copy/group/normal"], 39 | [:write, "copy/all/usr/local/bin/hello", "world"], 40 | [:write, "Rootfs", "hello"], 41 | [:mkdir, "copy/rootfs/"], 42 | [:with_opts, { 43 | :push => false, 44 | :force => false, 45 | }] 46 | ] 47 | } 48 | 49 | # -- 50 | 51 | FS_LAYOUTS[:rootfs] = FS_LAYOUTS[ 52 | :scratch 53 | ] 54 | 55 | # -- 56 | 57 | FS_LAYOUTS.freeze 58 | attr_reader :hashes 59 | attr_reader :original_pwd 60 | attr_reader :context 61 | attr_reader :root 62 | 63 | # -- 64 | 65 | def initialize(context) 66 | @original_pwd = Dir.pwd 67 | @root = Pathutil.new(Dir.mktmpdir).realpath 68 | @context = context 69 | 70 | @hashes = HashWithIndifferentAccess.new({ 71 | :cli => {}, 72 | :init => { 73 | :name => "default" 74 | } 75 | }) 76 | 77 | rescue 78 | if @root && @root.exist? 79 | then @root.rm_rf 80 | end 81 | 82 | raise 83 | end 84 | 85 | # -- 86 | # Adds a tag to opts.yml properly, on your behalf. 87 | # -- 88 | 89 | def add_tag(name, group: :normal) 90 | return with_opts(:tags => { 91 | name => group 92 | }) 93 | end 94 | 95 | # -- 96 | # Adds an alias on your behalf, properly. 97 | # -- 98 | 99 | def add_alias(name, tag: :default) 100 | return with_opts(:aliases => { 101 | name => tag 102 | }) 103 | end 104 | 105 | # -- 106 | # Determines if a type and layout are valid to use, AKA within the list. 107 | # Example: mocked_repo.valid_layout?(:scratch, :simple) #=> true 108 | # -- 109 | 110 | def valid_layout?(type) 111 | return FS_LAYOUTS.key?( 112 | type 113 | ) 114 | end 115 | 116 | # -- 117 | # Initialize and write all of the files for a given layout, making it 118 | # possible to run `to_repo` and get back a valid repository to test. 119 | # type - the type of layout you wish to use. 120 | # lyout - the layout you are using. 121 | # 122 | # Example: 123 | # mocked_repo.init({ 124 | # :type => :normal, 125 | # :layout => :complex 126 | # ) 127 | # 128 | # File: /tmp/*/repos/*/copy 129 | # File: /tmp/*/repos/*/Dockerfile 130 | # File: /tmp/*/repos/*/copy/usr/local/bin/hello 131 | # File: /tmp/*/repos/*/opts.yml 132 | # -- 133 | 134 | def init(type: :normal) 135 | if !valid_layout?(type) 136 | raise ArgumentError, "Unknown layout type (#{ 137 | type 138 | })" 139 | 140 | else 141 | FS_LAYOUTS[@fs_layout = type].each do |(m, *a)| 142 | send m, *a 143 | end 144 | 145 | self 146 | end 147 | end 148 | 149 | # -- 150 | # Options being passed to `Repo`. 151 | # -- 152 | 153 | def with_cli_opts(args) 154 | @hashes[:cli] = @hashes[:cli].deep_merge( 155 | args.stringify 156 | ) 157 | 158 | self 159 | end 160 | 161 | # -- 162 | 163 | def with_repo_init(hash) 164 | @hashes[:init] = @hashes[:init].deep_merge( 165 | hash.stringify 166 | ) 167 | 168 | self 169 | end 170 | 171 | # -- 172 | 173 | def clear_opts 174 | @hashes[:opts] = {} 175 | repo_dir.join(Docker::Template::Meta.opts_file).rm_f 176 | self 177 | end 178 | 179 | # -- 180 | # Example: mocked_repo.with_opts(:hello => :world) 181 | # Write the hash data into the repos `opts.yml` 182 | # -- 183 | 184 | def with_opts(opts) 185 | @hashes[:opts] ||= repo_dir.join(Docker::Template::Meta.opts_file).read_yaml 186 | @hashes[:opts] = @hashes[:opts].deep_merge(opts.stringify) 187 | repo_dir.join("opts.yml").write( 188 | @hashes[:opts].to_yaml 189 | ) 190 | 191 | self 192 | end 193 | 194 | # -- 195 | 196 | def to_img 197 | Docker::Image.get( 198 | to_repo.to_s 199 | ) 200 | 201 | rescue => e 202 | if e.is_a?(Docker::Error::NotFoundError) 203 | then nil else raise e 204 | end 205 | end 206 | 207 | # -- 208 | 209 | def to_repo 210 | repo_dir 211 | Docker::Template::Repo.new( 212 | @hashes[:init].dup, @hashes[:cli].dup 213 | ) 214 | end 215 | 216 | # -- 217 | # to_rootfs, to_normal, to_scratch 218 | # -- 219 | 220 | %W(Scratch Normal Rootfs).each do |k| 221 | define_method "to_#{k.downcase}" do 222 | Docker::Template::Builder.const_get(k).new( 223 | to_repo 224 | ) 225 | end 226 | end 227 | 228 | # -- 229 | 230 | def empty 231 | @emptied = true 232 | @root.glob("*").map( 233 | &:rm_rf 234 | ) 235 | 236 | self 237 | end 238 | 239 | # -- 240 | # Example: mocked_repo.mkdir("hello") 241 | # Make an empty directory! 242 | # -- 243 | 244 | def mkdir(dir) 245 | repo_dir.join(dir).mkdir_p 246 | self 247 | end 248 | 249 | # -- 250 | # Example: mocked_rep.write("hello", "world") 251 | # Make a file and write data to it. 252 | # -- 253 | 254 | def write(file, data) 255 | path = repo_dir.join(file) 256 | path.dirname.mkdir_p 257 | path.write( 258 | data 259 | ) 260 | 261 | self 262 | end 263 | 264 | # -- 265 | # Example: mocked_repo.touch("my_file") 266 | # Create an empty file of your choosing. 267 | # -- 268 | 269 | def touch(file) 270 | repo_dir.join(file).touch 271 | self 272 | end 273 | 274 | # -- 275 | # Symlink a file within the current repository directory. 276 | # Example: mocked_repo.symlink("/etc", "etc") 277 | # -- 278 | 279 | def symlink(target, name) 280 | repo_dir.join(target).symlink(repo_dir.join( 281 | name 282 | )) 283 | 284 | self 285 | end 286 | 287 | # -- 288 | # Delete a file from the current repository directory. 289 | # Example: mocked_repo.delete("hello") 290 | # -- 291 | 292 | def delete(file) 293 | repo_dir.join(file).rm_r 294 | self 295 | end 296 | 297 | # -- 298 | # Link to a file outside of any known and recognized root. 299 | # Example: mocked_repo.symlink("/etc", "etc") 300 | # -- 301 | 302 | def external_symlink(target, name) 303 | Pathutil.new(target).symlink(repo_dir.join( 304 | name 305 | )) 306 | 307 | self 308 | end 309 | 310 | # -- 311 | 312 | def setup 313 | @context.allow(Dir).to @context.receive(:pwd).and_return @root if rspec? 314 | @context.allow(Docker::Template).to context.receive(:root).and_return @root if rspec? 315 | Docker::Template.stub(:root).and_return @root unless rspec? 316 | Dir.stub(:pwd).and_return @repo.to_s unless rspec? 317 | end 318 | 319 | # -- 320 | 321 | def teardown 322 | @root.rm_rf 323 | end 324 | 325 | # -- 326 | 327 | def rspec? 328 | @context.is_a?( 329 | RSpec::Core::ExampleGroup 330 | ) 331 | end 332 | 333 | # -- 334 | 335 | private 336 | def repo_dir 337 | rtn = @root if @fs_layout == :project 338 | rtn = @root.join(Docker::Template::Meta::DEFAULTS[:repos_dir], \ 339 | @hashes[:init][:name]) unless @fs_layout == :project 340 | rtn.mkdir_p unless rtn.exist? || emptied? 341 | rtn 342 | end 343 | 344 | # -- 345 | 346 | alias clear empty 347 | alias to_project to_normal 348 | rb_delegate :emptied?, :to => :@emptied, :type => :ivar, :bool => true 349 | rb_delegate :join, :to => :@root 350 | end 351 | end 352 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/auth_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Auth do 7 | include_context :repos 8 | 9 | before :all do 10 | class AuthPathutilWrapper 11 | def expand_path 12 | self 13 | end 14 | 15 | # 16 | 17 | def read_json 18 | return { 19 | "auths" => { 20 | "server.com" => { 21 | "email" => "user@example.com", 22 | "auth" => Base64.encode64( 23 | "username:password" 24 | ) 25 | } 26 | } 27 | } 28 | end 29 | end 30 | end 31 | 32 | # 33 | 34 | before do 35 | allow(Pathutil).to receive(:new).and_call_original 36 | allow(Docker).to receive(:authenticate!).and_return(nil) 37 | allow_any_instance_of(described_class).to receive(:auth_with_env?).and_return false 38 | allow(Pathutil).to receive(:new).with("~/.docker/config.json") \ 39 | .and_return(AuthPathutilWrapper.new) 40 | end 41 | 42 | # 43 | 44 | describe "#auth", :skip_auth => true do 45 | context "when it cannot authenticate" do 46 | before do 47 | allow(Docker).to receive :authenticate! do 48 | raise Docker::Error::AuthenticationError 49 | end 50 | end 51 | 52 | it "should throw" do 53 | expect { described_class.new(mocked_repo.to_repo).auth }.to raise_error( 54 | Docker::Template::Error::UnsuccessfulAuth 55 | ) 56 | end 57 | end 58 | end 59 | 60 | # 61 | 62 | describe "auth_from_env" do 63 | before do 64 | allow_any_instance_of(described_class).to receive(:auth_with_env?).and_return true 65 | allow(ENV).to receive(:[]).with("DOCKER_SERVER").and_return("eserver.com") 66 | allow(ENV).to receive(:[]).with("DOCKER_EMAIL").and_return("euser@example.com") 67 | allow(ENV).to receive(:[]).with("DOCKER_PASSWORD").and_return("epassword") 68 | allow(ENV).to receive(:[]).with("DOCKER_USERNAME").and_return("eusername") 69 | end 70 | 71 | # 72 | 73 | it "should authenticate with those credentials" do 74 | expect(Docker).to receive(:authenticate!).with({ 75 | "username" => "eusername", 76 | "serveraddress" => "eserver.com", 77 | "email" => "euser@example.com", 78 | "password" => "epassword" 79 | }) 80 | end 81 | 82 | # 83 | 84 | context "when the user doesn't set a server" do 85 | before do 86 | allow(ENV).to receive(:[]).with("bamboo_dockerServer").and_return nil 87 | allow(ENV).to receive(:[]).with("DOCKER_SERVER").and_return( 88 | nil 89 | ) 90 | end 91 | 92 | # 93 | 94 | it "should use the default" do 95 | expect(Docker).to receive(:authenticate!).with({ 96 | "username" => "eusername", 97 | "serveraddress" => described_class::DEFAULT_SERVER, 98 | "email" => "euser@example.com", 99 | "password" => "epassword" 100 | }) 101 | end 102 | end 103 | end 104 | 105 | # 106 | 107 | context "_hub_config" do 108 | context "and the user has ~/.docker/config.json" do 109 | it "should read the file" do 110 | expect(Pathutil).to receive(:new).with("~/.docker/config.json").and_return( 111 | AuthPathutilWrapper.new 112 | ) 113 | end 114 | 115 | # 116 | 117 | it "should auth with the credentials" do 118 | expect(Docker).to receive(:authenticate!).at_least(:once).with({ 119 | "username" => "username", 120 | "serveraddress" => "server.com", 121 | "email" => "user@example.com", 122 | "password" => "password" 123 | }) 124 | end 125 | end 126 | end 127 | 128 | # 129 | 130 | after do |ex| 131 | unless ex.metadata[:skip_auth] 132 | described_class.new(mocked_repo.to_repo).auth 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/builder/normal_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Builder::Normal, :type => :normal do 7 | include_contexts :docker, :repos 8 | 9 | # 10 | 11 | subject do 12 | mocked_repo.with_repo_init("tag" => "latest") 13 | mocked_repo.to_normal 14 | end 15 | 16 | # 17 | 18 | it "should allow projects" do 19 | expect(described_class.projects_allowed?).to eq( 20 | true 21 | ) 22 | end 23 | 24 | # 25 | 26 | it "should allow what Dockerfiles it allows" do 27 | expect(described_class.files).not_to( 28 | be_empty 29 | ) 30 | end 31 | 32 | # 33 | 34 | describe "#copy_dockerfile" do 35 | context "when the it's a project build", :type => :project do 36 | it "should pull the Dockerfile from the root" do 37 | expect(Docker::Template).to receive(:root) \ 38 | .and_call_original 39 | end 40 | end 41 | 42 | # 43 | 44 | it "should pull the Dockerfile from the repo root" do 45 | expect(subject.repo).to receive(:root) \ 46 | .and_call_original 47 | end 48 | 49 | # 50 | 51 | it "should create an ERB context" do 52 | expect(ERB::Context).to receive(:new) \ 53 | .and_call_original 54 | end 55 | 56 | # 57 | 58 | after do 59 | subject.send(:setup_context) 60 | subject.teardown 61 | end 62 | end 63 | 64 | # 65 | 66 | describe "#cache_context" do 67 | context "when cache = false" do 68 | before do 69 | subject.repo.meta.merge!({ 70 | "cache" => false 71 | }) 72 | end 73 | 74 | # 75 | 76 | context do 77 | before do 78 | silence_io do 79 | subject.send( 80 | :cache_context 81 | ) 82 | end 83 | end 84 | 85 | # 86 | 87 | it "should not copy all the files" do 88 | expect(subject.repo.cache_dir).not_to( 89 | exist 90 | ) 91 | end 92 | end 93 | 94 | # 95 | 96 | context do 97 | it "should not call cache to copy it" do 98 | expect(Docker::Template::Cache).not_to receive( 99 | :context 100 | ) 101 | end 102 | 103 | # 104 | 105 | after do 106 | subject.send( 107 | :cache_context 108 | ) 109 | end 110 | end 111 | end 112 | 113 | # 114 | 115 | context "when cache = true" do 116 | before do 117 | subject.repo.meta.merge!({ 118 | "cache" => true 119 | }) 120 | end 121 | 122 | # 123 | 124 | context do 125 | before do 126 | silence_io do 127 | subject.send :setup_context 128 | subject.send :cache_context 129 | end 130 | end 131 | 132 | # 133 | 134 | it "should copy all the files" do 135 | expect(subject.repo.cache_dir).to( 136 | exist 137 | ) 138 | end 139 | end 140 | 141 | # 142 | 143 | context do 144 | it "should call the cache to copy it" do 145 | expect(Docker::Template::Cache).to receive(:context).and_return( 146 | nil 147 | ) 148 | end 149 | 150 | # 151 | 152 | after do 153 | subject.send( 154 | :cache_context 155 | ) 156 | end 157 | end 158 | 159 | # 160 | 161 | after do 162 | subject.teardown 163 | subject.repo.meta.merge!({ 164 | "cache" => false 165 | }) 166 | end 167 | end 168 | end 169 | 170 | # 171 | 172 | describe "#teardown" do 173 | before do 174 | subject.send :setup_context 175 | subject.teardown 176 | end 177 | 178 | # 179 | 180 | it "should delete the context folder" do 181 | expect(subject.instance_variable_get(:@context)).not_to( 182 | exist 183 | ) 184 | end 185 | 186 | # 187 | 188 | context "(img: true)" do 189 | before do 190 | subject.instance_variable_set( 191 | :@img, image_mock 192 | ) 193 | end 194 | 195 | # 196 | 197 | it "should try to delete the image" do 198 | expect(image_mock).to receive( 199 | :delete 200 | ) 201 | end 202 | 203 | # 204 | 205 | after do 206 | subject.teardown(img: true) 207 | subject.remove_instance_variable(:@img) 208 | end 209 | end 210 | end 211 | 212 | # 213 | 214 | describe "#setup_context" do 215 | before do 216 | subject.send( 217 | :setup_context 218 | ) 219 | end 220 | 221 | # 222 | 223 | it "should copy the Dockerfile" do 224 | expect(subject.instance_variable_get(:@context).find.map(&:to_s)).to include match( 225 | /Dockerfile\Z/ 226 | ) 227 | end 228 | 229 | # 230 | 231 | after do 232 | subject.teardown 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/builder/rootfs_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | RSpec.describe Docker::Template::Builder::Rootfs do 7 | include_contexts :docker, :repos 8 | 9 | # 10 | 11 | subject do 12 | mocked_repo.to_rootfs 13 | end 14 | 15 | # 16 | 17 | before do 18 | mocked_repo.with_repo_init({ 19 | "tag" => "default" 20 | }) 21 | end 22 | 23 | # 24 | 25 | it "should flag itself as a sub-builder" do 26 | expect(described_class.sub?).to eq( 27 | true 28 | ) 29 | end 30 | 31 | # 32 | 33 | it "should not allow projects" do 34 | expect(described_class.projects_allowed?).to( 35 | be_falsey 36 | ) 37 | end 38 | 39 | # 40 | 41 | it "should support the types of Dockerfiles it supports" do 42 | expect(described_class.files).not_to( 43 | be_empty 44 | ) 45 | end 46 | 47 | # 48 | 49 | describe "#data" do 50 | it "should add the FROM line" do 51 | expect(subject.data).to match( 52 | %r!\AFROM [a-z]+/alpine! 53 | ) 54 | end 55 | end 56 | 57 | # 58 | 59 | describe "#copy_rootfs" do 60 | before do 61 | subject.send( 62 | :setup_context 63 | ) 64 | end 65 | 66 | # 67 | 68 | it "should copy", :type => :rootfs, :layout => :complex do 69 | expect_any_instance_of(Pathutil).to receive(:safe_copy).and_return( 70 | nil 71 | ) 72 | end 73 | 74 | # 75 | 76 | after do 77 | subject.send :copy_rootfs 78 | subject.teardown 79 | end 80 | end 81 | 82 | # 83 | 84 | describe "#teardown" do 85 | before do 86 | silence_io do 87 | subject.build 88 | end 89 | end 90 | 91 | # 92 | 93 | it "should delete the context it created" do 94 | expect(subject.instance_variable_get(:@context)).not_to( 95 | exist 96 | ) 97 | end 98 | 99 | # 100 | 101 | context "(img: true)" do 102 | context do 103 | it "should delete the image" do 104 | expect(image_mock).to receive(:delete).and_return( 105 | nil 106 | ) 107 | end 108 | 109 | # 110 | 111 | after do 112 | subject.teardown({ 113 | :img => true 114 | }) 115 | end 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/builder/scratch_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Builder::Scratch do 7 | include_contexts :docker, :repos 8 | 9 | # 10 | 11 | before do 12 | mocked_repo.init({ 13 | :type => :normal 14 | }) 15 | end 16 | 17 | # 18 | 19 | subject do 20 | mocked_repo.with_repo_init("tag" => "latest") 21 | mocked_repo.to_scratch 22 | end 23 | 24 | # 25 | 26 | before do 27 | allow(subject).to receive(:create_args).and_return({}) 28 | allow(subject).to receive(:verify_context).and_return nil 29 | allow(subject).to receive(:start_args).and_return({}) 30 | end 31 | 32 | # 33 | 34 | it "should not allow projects" do 35 | expect(described_class.projects_allowed?).to( 36 | be_falsey 37 | ) 38 | end 39 | 40 | # 41 | 42 | it "should support the types of Dockerfiles it supports" do 43 | expect(described_class.files).not_to( 44 | be_empty 45 | ) 46 | end 47 | 48 | # 49 | 50 | describe "#data" do 51 | before do 52 | subject.send( 53 | :setup_context 54 | ) 55 | end 56 | 57 | # 58 | 59 | it "adds the TARGZ file" do 60 | expect(subject.data).to match( 61 | %r!^ADD .*\.tar\.gz /$!m 62 | ) 63 | end 64 | 65 | # 66 | 67 | it "adds the MAINTAINER" do 68 | expect(subject.data).to match %r!MAINTAINER #{Regexp.escape( 69 | subject.repo.meta["maintainer"] 70 | )}! 71 | end 72 | 73 | # 74 | 75 | it "should not return empty data" do 76 | expect(subject.data).not_to( 77 | be_empty 78 | ) 79 | end 80 | 81 | # 82 | 83 | after do 84 | subject.teardown 85 | end 86 | end 87 | 88 | # 89 | 90 | describe "#setup_context" do 91 | before do 92 | subject.send( 93 | :setup_context 94 | ) 95 | end 96 | 97 | # 98 | 99 | it "should copy the Dockerfile" do 100 | expect(subject.instance_variable_get(:@context).find.map(&:to_s)).to include match( 101 | /\/Dockerfile\Z/ 102 | ) 103 | end 104 | 105 | # 106 | 107 | after do 108 | subject.teardown 109 | end 110 | end 111 | 112 | # 113 | 114 | describe "#teardown" do 115 | before do 116 | silence_io do 117 | subject.build 118 | end 119 | end 120 | 121 | # 122 | 123 | context "(img: true)" do 124 | it "should delete the image" do 125 | expect(image_mock).to receive( 126 | :delete 127 | ) 128 | end 129 | 130 | # 131 | 132 | after do 133 | subject.teardown({ 134 | :img => true 135 | }) 136 | end 137 | end 138 | 139 | # 140 | 141 | context do 142 | let :pathname do 143 | subject.instance_variable_get( 144 | :@context 145 | ) 146 | end 147 | 148 | # 149 | 150 | before do 151 | subject.teardown 152 | end 153 | 154 | # 155 | 156 | it "should remove the context" do 157 | expect(pathname).not_to( 158 | exist 159 | ) 160 | end 161 | end 162 | end 163 | 164 | # 165 | 166 | describe "#build_context" do 167 | before do 168 | allow_any_instance_of(Docker::Template::Builder::Rootfs).to receive(:build).and_return( 169 | nil 170 | ) 171 | end 172 | 173 | after do |ex| 174 | unless ex.metadata[:nobuild] 175 | subject.send( 176 | :build_context 177 | ) 178 | end 179 | end 180 | 181 | # 182 | 183 | it "should build the rootfs context" do 184 | expect(subject.rootfs).to receive( 185 | :build 186 | ) 187 | end 188 | 189 | # 190 | 191 | it "should stop the rootfs container once it's done" do 192 | expect(container_mock).to receive( 193 | :stop 194 | ) 195 | end 196 | 197 | # 198 | 199 | it "should teardown the rootfs image" do 200 | expect(subject.rootfs).to receive( 201 | :teardown 202 | ) 203 | end 204 | 205 | # 206 | 207 | it "should delete the rootfs container" do 208 | expect(container_mock).to receive( 209 | :delete 210 | ) 211 | end 212 | 213 | # 214 | 215 | context "when an image exists badly" do 216 | before do 217 | allow(container_mock).to receive :json do 218 | { 219 | "State" => { 220 | "ExitCode" => 1 221 | } 222 | } 223 | end 224 | end 225 | 226 | # 227 | 228 | it "should raise an error", :nobuild do 229 | expect { subject.send :build_context }.to raise_error( 230 | Docker::Template::Error::BadExitStatus 231 | ) 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/builder_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Builder do 7 | include_contexts :docker, :repos 8 | 9 | # 10 | 11 | after do |ex| 12 | unless ex.metadata[:skip_teardown] 13 | subject.teardown 14 | end 15 | end 16 | 17 | # 18 | 19 | subject do |ex| 20 | mocked_repo.send( 21 | "to_#{ex.metadata[:type]}" 22 | ) 23 | end 24 | 25 | # 26 | 27 | before do 28 | # rubocop:disable Style/SpaceInsideParens 29 | allow(subject).to receive(:verify_context).and_return nil 30 | allow(subject).to receive( :build_context).and_return nil 31 | # rubocop:enable Style/SpaceInsideParens 32 | end 33 | 34 | # 35 | 36 | describe "#.sub?" do 37 | it "is false by default..." do 38 | expect(described_class.sub?).to eq( 39 | false 40 | ) 41 | end 42 | end 43 | 44 | # 45 | 46 | describe ".files" do 47 | it "should be a reference method and empty" do 48 | expect(described_class.files).to( 49 | be_empty 50 | ) 51 | end 52 | end 53 | 54 | # 55 | 56 | describe ".projects_allowed?" do 57 | context "when the user wishes to allow project builds" do 58 | before :all do 59 | class BuilderTestMock < Docker::Template::Builder 60 | projects_allowed! 61 | end 62 | end 63 | 64 | it "should be true" do 65 | expect(BuilderTestMock.projects_allowed?).to eq( 66 | true 67 | ) 68 | end 69 | 70 | after :all do 71 | Object.send( 72 | :remove_const, :BuilderTestMock 73 | ) 74 | end 75 | end 76 | 77 | # 78 | 79 | it "should be nil by default" do 80 | expect(described_class.projects_allowed?).to( 81 | be_falsey 82 | ) 83 | end 84 | end 85 | 86 | # 87 | 88 | describe ".all" do 89 | it "should hold a list of all classes" do 90 | expect(described_class.all).not_to( 91 | be_empty 92 | ) 93 | end 94 | end 95 | 96 | # 97 | 98 | describe "#alias?" do 99 | it "should return false if the repository is not an alias" do 100 | expect(mocked_repo.to_normal.alias?).to eq( 101 | false 102 | ) 103 | end 104 | 105 | # 106 | 107 | context "when a simple alias" do 108 | before do 109 | mocked_repo.add_alias( 110 | "hello" 111 | ) 112 | end 113 | 114 | # 115 | 116 | it "should return true" do 117 | expect(mocked_repo.with_repo_init("tag" => "hello").to_normal.alias?).to eq( 118 | true 119 | ) 120 | end 121 | end 122 | 123 | # 124 | 125 | context "when a complex alias" do 126 | before do 127 | mocked_repo.with_repo_init "tag" => "hello" 128 | mocked_repo.add_alias "hello" 129 | mocked_repo.with_opts({ 130 | "env" => { 131 | "tag" => { 132 | "hello" => [ 133 | "world" 134 | ] 135 | } 136 | } 137 | }) 138 | end 139 | 140 | # 141 | 142 | it "should return false" do 143 | expect(mocked_repo.to_normal.alias?).to eq( 144 | false 145 | ) 146 | end 147 | end 148 | end 149 | 150 | # 151 | 152 | describe "#rootfs?" do 153 | it "should return true if it's a rootfs" do 154 | expect(mocked_repo.to_rootfs.rootfs?).to eq( 155 | true 156 | ) 157 | end 158 | 159 | # 160 | 161 | context "when it's not a rootfs" do 162 | it "should return false" do 163 | expect(mocked_repo.to_normal.rootfs?).to eq( 164 | false 165 | ) 166 | end 167 | end 168 | end 169 | 170 | # 171 | 172 | describe "#normal?" do 173 | it "should return true if it's a normal" do 174 | expect(mocked_repo.to_normal.normal?).to eq( 175 | true 176 | ) 177 | end 178 | 179 | # 180 | 181 | context "when it's not normal" do 182 | it "should return false" do 183 | expect(mocked_repo.to_rootfs.normal?).to eq( 184 | false 185 | ) 186 | end 187 | end 188 | end 189 | 190 | # 191 | 192 | describe "#scratch?" do 193 | context do 194 | before do 195 | mocked_repo.with_opts({ 196 | :type => :scratch 197 | }) 198 | end 199 | 200 | # 201 | 202 | it "should return true if it's a scratch" do 203 | expect(mocked_repo.to_scratch.scratch?).to eq( 204 | true 205 | ) 206 | end 207 | end 208 | 209 | context "when it's not a scratch" do 210 | it "should return false" do 211 | expect(mocked_repo.to_normal.scratch?).to eq( 212 | false 213 | ) 214 | end 215 | end 216 | end 217 | 218 | # 219 | 220 | describe "#aliased_img" do 221 | context "when there is no alias" do 222 | it "should return nothing" do 223 | expect(mocked_repo.to_normal.aliased_img).to eq( 224 | nil 225 | ) 226 | end 227 | end 228 | 229 | # 230 | 231 | context "when the image is an alias of another image" do 232 | before do 233 | mocked_repo.add_alias "world" 234 | mocked_repo.with_repo_init({ 235 | "tag" => "world" 236 | }) 237 | end 238 | 239 | it "should try and pull the image" do 240 | expect(Docker::Image).to receive(:get).and_return( 241 | nil 242 | ) 243 | end 244 | 245 | # 246 | 247 | after do 248 | mocked_repo.to_normal \ 249 | .aliased_img 250 | end 251 | end 252 | end 253 | 254 | # 255 | 256 | describe "#push" do 257 | before do 258 | subject.repo.meta.merge!({ 259 | "push" => true 260 | }) 261 | end 262 | 263 | # 264 | 265 | after do 266 | silence_io { subject.build } 267 | subject.repo.meta.merge!({ 268 | "push" => false 269 | }) 270 | end 271 | 272 | # 273 | 274 | it "should try to auth" do 275 | expect_any_instance_of(Docker::Template::Auth).to \ 276 | receive(:auth).and_return( 277 | nil 278 | ) 279 | end 280 | 281 | # 282 | 283 | context do 284 | before do 285 | allow_any_instance_of(Docker::Template::Auth).to \ 286 | receive(:auth).and_return( 287 | nil 288 | ) 289 | end 290 | 291 | # 292 | 293 | it "should try to push" do 294 | expect(image_mock).to receive(:push).and_return( 295 | nil 296 | ) 297 | end 298 | end 299 | 300 | # 301 | 302 | context "when push == false" do 303 | before do 304 | subject.repo.meta.merge!({ 305 | "push" => false 306 | }) 307 | end 308 | 309 | # 310 | 311 | it "should not try to push the repo" do 312 | expect(image_mock).not_to receive( 313 | :push 314 | ) 315 | end 316 | end 317 | end 318 | 319 | # 320 | 321 | context do 322 | before do 323 | subject.send( 324 | :setup_context 325 | ) 326 | end 327 | 328 | # 329 | 330 | describe "#copy_global" do 331 | context "when it's a scratch image" do 332 | it "should copy", :type => :scratch do 333 | expect_any_instance_of(Pathutil).to receive(:safe_copy).and_return( 334 | nil 335 | ) 336 | end 337 | end 338 | 339 | # 340 | 341 | context "when it's a normal image" do 342 | it "should copy", :type => :normal do 343 | expect_any_instance_of(Pathutil).to receive(:safe_copy).and_return( 344 | nil 345 | ) 346 | end 347 | end 348 | 349 | # 350 | 351 | context "when it's a rootfs image" do 352 | it "should not copy", :type => :rootfs do 353 | expect_any_instance_of(Pathutil).not_to receive( 354 | :safe_copy 355 | ) 356 | end 357 | end 358 | 359 | # 360 | 361 | after do 362 | subject.send( 363 | :copy_global 364 | ) 365 | end 366 | end 367 | 368 | # 369 | 370 | describe "#copy_project", :type => :project do 371 | it "should copy" do 372 | expect(Docker::Template.root).to receive(:safe_copy).and_return( 373 | nil 374 | ) 375 | end 376 | 377 | # 378 | 379 | it "should pass ignores" do 380 | hash = { :root => anything, :ignore => anything } 381 | expect(Docker::Template.root).to receive(:safe_copy).with(anything, hash).and_return( 382 | nil 383 | ) 384 | end 385 | 386 | # 387 | 388 | after do 389 | subject.send( 390 | :copy_project 391 | ) 392 | end 393 | end 394 | 395 | # 396 | 397 | shared_examples :copy do 398 | context "when it's scratch" do 399 | it "should copy", :type => :scratch do 400 | expect_any_instance_of(Pathutil).to receive(:safe_copy).and_return( 401 | nil 402 | ) 403 | end 404 | end 405 | 406 | # 407 | 408 | context "when it's normal" do 409 | it "should copy", :type => :normal do 410 | expect_any_instance_of(Pathutil).to receive(:safe_copy).and_return( 411 | nil 412 | ) 413 | end 414 | end 415 | 416 | # 417 | 418 | context "when it's a rootfs" do 419 | it "should not copy", :type => :rootfs do 420 | expect_any_instance_of(Pathutil).not_to receive( 421 | :safe_copy 422 | ) 423 | end 424 | 425 | # 426 | 427 | context "when simple_copy?", :simple_copy do 428 | it "should not copy", :type => :rootfs, :layout => :simple do 429 | expect_any_instance_of(Pathutil).not_to receive( 430 | :safe_copy 431 | ) 432 | end 433 | end 434 | end 435 | end 436 | 437 | # 438 | 439 | describe "#copy_tag" do 440 | it_behaves_like( 441 | :copy 442 | ) 443 | 444 | # 445 | 446 | after do 447 | subject.send( 448 | :copy_tag 449 | ) 450 | end 451 | end 452 | 453 | describe "#copy_group" do 454 | before do 455 | subject.repo.meta["tags"] = { 456 | "latest" => "normal" 457 | } 458 | end 459 | 460 | after do 461 | subject.send( 462 | :copy_group 463 | ) 464 | end 465 | 466 | # 467 | 468 | it_behaves_like( 469 | :copy 470 | ) 471 | end 472 | 473 | describe "#copy_all" do 474 | after do 475 | subject.send( 476 | :copy_all 477 | ) 478 | end 479 | 480 | it_behaves_like( 481 | :copy 482 | ) 483 | end 484 | end 485 | 486 | # 487 | 488 | describe "#build" do 489 | after do |ex| 490 | unless ex.metadata[:skip] 491 | ex.metadata[:noisey] ? subject.build : silence_io do 492 | subject.build 493 | end 494 | end 495 | end 496 | 497 | # 498 | 499 | shared_examples :build do 500 | context do 501 | before do 502 | allow(subject.repo).to receive(:buildable?).and_return false 503 | allow(subject).to receive(:build) \ 504 | .and_call_original 505 | end 506 | 507 | # 508 | 509 | it "should check if the repo is buildable" do 510 | expect(subject.repo).to receive(:buildable?).and_return( 511 | nil 512 | ) 513 | end 514 | end 515 | 516 | # 517 | 518 | context "when building? is set to false" do 519 | before do 520 | allow(subject.repo).to receive(:buildable?).and_return false 521 | allow(subject).to receive(:build) \ 522 | .and_call_original 523 | end 524 | 525 | # 526 | 527 | it "should not build" do 528 | expect(subject).not_to receive( 529 | :chdir_build 530 | ) 531 | end 532 | end 533 | 534 | # 535 | 536 | it "should build from the context" do 537 | expect(Docker::Image).to receive( 538 | :build_from_dir 539 | ) 540 | end 541 | 542 | # 543 | 544 | it "should teardown", :skip_teardown => true do 545 | expect(subject).to receive(:teardown) \ 546 | .and_call_original 547 | end 548 | 549 | # 550 | 551 | it "should not throw any errors" do 552 | expect { silence_io { subject.build }}.not_to( 553 | raise_error 554 | ) 555 | end 556 | 557 | # 558 | 559 | it "should notify of the build", :noisey do 560 | expect(Docker::Template::Notify).to receive(:build).and_return( 561 | nil 562 | ) 563 | end 564 | 565 | # 566 | 567 | it "should clear the screen" do 568 | expect(Simple::Ansi).to receive(:clear).and_return( 569 | nil 570 | ) 571 | end 572 | end 573 | 574 | # 575 | 576 | context "when @subject.type == normal", :type => :normal do 577 | it_behaves_like( 578 | :build 579 | ) 580 | end 581 | 582 | # 583 | 584 | context "when subject.type == scratch" do 585 | before do 586 | # rubocop:disable Style/SpaceInsideParens 587 | allow(subject).to receive(:create_args).and_return({}) 588 | allow(subject).to receive( :start_args).and_return({}) 589 | # rubocop:enable Style/SpaceInsideParens 590 | end 591 | 592 | it_behaves_like( 593 | :build 594 | ) 595 | end 596 | end 597 | end 598 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/cache_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Cache do 7 | include_context :repos 8 | 9 | # 10 | 11 | describe "#context" do 12 | context "when told to cache a non-aliased image" do 13 | before do 14 | subject.send :setup 15 | silence_io do 16 | described_class.context( 17 | subject, subject.context 18 | ) 19 | end 20 | end 21 | 22 | # 23 | 24 | subject do 25 | mocked_repo.to_normal 26 | end 27 | 28 | # 29 | 30 | it "should cache the context", :type => :normal do 31 | expect(subject.repo.cache_dir).to( 32 | exist 33 | ) 34 | end 35 | 36 | # 37 | 38 | after do 39 | subject.teardown 40 | end 41 | end 42 | 43 | # 44 | 45 | context "when told to cache an aliased image" do 46 | subject do 47 | mocked_repo.to_normal 48 | end 49 | 50 | # 51 | 52 | before do 53 | mocked_repo.add_alias :hello 54 | mocked_repo.with_repo_init :tag => :hello 55 | mocked_repo.with_opts :cache => true 56 | 57 | silence_io do 58 | subject.aliased_repo.builder.send :setup 59 | described_class.aliased_context( 60 | subject 61 | ) 62 | end 63 | end 64 | 65 | # 66 | 67 | it "should copy it's parent's context", :type => :normal do 68 | expect(subject.repo.cache_dir).to( 69 | exist 70 | ) 71 | end 72 | 73 | # 74 | 75 | after do 76 | subject.aliased_repo.builder.teardown 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/cli/build_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | require "docker/template/cli" 7 | describe Docker::Template::CLI::Build do 8 | include_context :repos 9 | 10 | before do 11 | allow(Docker::Template::Repo).to receive(:build).and_return( 12 | nil 13 | ) 14 | end 15 | 16 | # 17 | 18 | let :parser do 19 | Docker::Template::Parser 20 | end 21 | 22 | # 23 | 24 | subject do 25 | described_class.new( 26 | [], {} 27 | ) 28 | end 29 | 30 | # 31 | 32 | describe "#initialize", :start => false do 33 | it "pulls the repositories" do 34 | expect(parser).to receive(:new).with([], {}) \ 35 | .and_call_original 36 | end 37 | 38 | # 39 | 40 | it "parses the repositories" do 41 | expect_any_instance_of(parser).to receive(:parse) \ 42 | .and_call_original 43 | end 44 | 45 | # 46 | 47 | after do 48 | described_class.new( 49 | [], {} 50 | ) 51 | end 52 | end 53 | 54 | # 55 | 56 | describe "#start" do 57 | it "profiles by default" do 58 | expect(subject).to receive(:_profile) \ 59 | .and_call_original 60 | end 61 | end 62 | 63 | # 64 | 65 | context "--diff or diff: true", :ruby => "!jruby" do 66 | it "should reselect the repositories" do 67 | expect(subject).to receive(:changed!).and_return( 68 | [] 69 | ) 70 | end 71 | 72 | # 73 | 74 | subject do 75 | described_class.new([], { 76 | :diff => true 77 | }) 78 | end 79 | end 80 | 81 | # 82 | 83 | context "--exclude or exclude: []", :ruby => "!jruby" do 84 | it "should reselect the repositories" do 85 | expect(subject).to receive(:exclude!).and_return( 86 | [] 87 | ) 88 | end 89 | 90 | # 91 | 92 | subject do 93 | described_class.new([], { 94 | :exclude => [ 95 | :hello 96 | ] 97 | }) 98 | end 99 | end 100 | 101 | # 102 | 103 | context "--profile or profile: true", :ruby => "!jruby" do 104 | before :all do 105 | require "memory_profiler" 106 | end 107 | 108 | # 109 | 110 | subject do 111 | described_class.new([], { 112 | :profile => true 113 | }) 114 | end 115 | 116 | # 117 | 118 | before :all do 119 | class ProfilerMock 120 | def pretty_print(*) 121 | return 122 | end 123 | end 124 | end 125 | 126 | # 127 | 128 | before do 129 | allow(MemoryProfiler).to receive(:report) \ 130 | .and_return( 131 | ProfilerMock.new 132 | ) 133 | end 134 | 135 | # 136 | 137 | it "should profile" do 138 | expect(MemoryProfiler).to receive(:report) \ 139 | .and_return( 140 | ProfilerMock.new 141 | ) 142 | end 143 | 144 | # 145 | 146 | it "should report" do 147 | expect_any_instance_of(ProfilerMock).to receive( 148 | :pretty_print 149 | ) 150 | end 151 | end 152 | 153 | # 154 | 155 | describe "#git_reselect_repos", :start => false, :ruby => "!jruby" do 156 | before do 157 | # Require uses #_require so we need to shim that out. 158 | allow(Docker::Template).to receive(:require).with("rugged").and_return( 159 | true 160 | ) 161 | end 162 | 163 | # 164 | 165 | let :git do 166 | require "rugged" 167 | Rugged::Repository.init_at( 168 | mocked_repo.root.to_s 169 | ) 170 | end 171 | 172 | # 173 | 174 | before do 175 | git.config.store "user.email", "user@example.com" 176 | git.config.store "user.name", "Some User" 177 | git.index.add_all 178 | 179 | Rugged::Commit.create git, { 180 | :message => "Init.", 181 | :tree => git.index.write_tree(git), 182 | :update_ref => "HEAD", 183 | :parents => [] 184 | } 185 | 186 | mocked_repo.add_tag "latest" 187 | mocked_repo.join("repos/hello").mkdir_p 188 | mocked_repo.join("repos/hello/opts.yml").write({ 189 | "tags" => { 190 | "latest" => "normal" 191 | } 192 | }.to_yaml) 193 | end 194 | 195 | # 196 | 197 | it "should pull repositories from the last commit" do 198 | expect_any_instance_of(Rugged::Repository).to receive(:last_commit) \ 199 | .and_call_original 200 | end 201 | 202 | # 203 | 204 | it "should return all modified repositories" do 205 | expect(subject.changed!.count).to eq 1 206 | expect(subject.changed!.first.name).to eq( 207 | "default" 208 | ) 209 | end 210 | 211 | # 212 | 213 | context "when argv = [val, val]" do 214 | it "should drop repos from that list that are not modified" do 215 | expect(described_class.new(%w(hello default), {}).changed!.count).to eq( 216 | 1 217 | ) 218 | end 219 | end 220 | 221 | # 222 | 223 | after do 224 | subject.changed! 225 | end 226 | end 227 | 228 | # 229 | 230 | after do |ex| 231 | unless ex.metadata[:start] == false 232 | subject.start 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/cli/list_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | require "active_support/core_ext/string/strip" 7 | describe Docker::Template::CLI::List do 8 | include_contexts :repos 9 | 10 | # 11 | 12 | subject do 13 | Simple::Ansi.strip(described_class.build).gsub( 14 | /(├─ |│)/, "" 15 | ) 16 | end 17 | 18 | # 19 | 20 | before do 21 | mocked_repo.clear_opts.with_opts({ 22 | :user => :user, 23 | :tags => { 24 | :tag => :normal 25 | } 26 | }) 27 | end 28 | 29 | # 30 | 31 | it "should color everything for readability" do 32 | expect(Simple::Ansi.has?(described_class.build)).to eq( 33 | true 34 | ) 35 | end 36 | 37 | # 38 | 39 | it "should output user, repo, and tags" do 40 | expect(subject).to eq <<-STR.strip_heredoc 41 | [user] user 42 | [repo] default 43 | [tag] tag 44 | STR 45 | end 46 | 47 | # 48 | 49 | context "when given a true remote" do 50 | before do 51 | mocked_repo.with_opts({ 52 | :aliases => { 53 | :alias => "remote/repo:tag" 54 | } 55 | }) 56 | end 57 | 58 | # 59 | 60 | it "should output aliases under that true remote" do 61 | expect(subject).to eq <<-STR.strip_heredoc 62 | [user] user 63 | [repo] default 64 | [tag] tag 65 | [remote] remote/repo:tag 66 | [alias] alias 67 | STR 68 | end 69 | end 70 | 71 | # 72 | 73 | context "when a tag has aliases" do 74 | before do 75 | mocked_repo.with_opts({ 76 | :aliases => { 77 | :alias => :tag 78 | } 79 | }) 80 | end 81 | 82 | # 83 | 84 | it "should output the aliases inside of the tag" do 85 | expect(subject).to eq <<-STR.strip_heredoc 86 | [user] user 87 | [repo] default 88 | [tag] tag 89 | [alias] alias 90 | STR 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/error_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Error do 7 | it { is_expected.to have_const :BadRepoName } 8 | it { is_expected.to have_const :BadExitStatus } 9 | it { is_expected.to have_const :NoSetupContext } 10 | it { is_expected.to have_const :UnsuccessfulAuth } 11 | it { is_expected.to have_const :InvalidTargzFile } 12 | it { is_expected.to have_const :InvalidYAMLFile } 13 | it { is_expected.to have_const :NotImplemented } 14 | it { is_expected.to have_const :RepoNotFound } 15 | it { is_expected.to have_const :ImageNotFound } 16 | end 17 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/logger_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Logger do 7 | subject do 8 | described_class 9 | end 10 | 11 | # 12 | 13 | def log(type, what) 14 | io = capture_io { described_class.new.api(what.to_json) } 15 | type == :both ? io : io[type] 16 | end 17 | 18 | # 19 | 20 | include_context :repos do 21 | describe "#output" do 22 | before do 23 | mocked_repo.with_opts("log_filters" => [ 24 | /hello/ 25 | ]) 26 | end 27 | 28 | it "should be able to filter messages" do 29 | expect(described_class.new(mocked_repo.to_repo).send(:output, "hello world")).to( 30 | be_truthy 31 | ) 32 | end 33 | end 34 | end 35 | 36 | # 37 | 38 | describe "#stdout" do 39 | it "should default to using .stdout" do 40 | expect(described_class.new.instance_variable_get(:@stdout)).to eq( 41 | described_class.stdout 42 | ) 43 | end 44 | 45 | it "should be overrideable" do 46 | val = described_class.new(nil, "hello").instance_variable_get(:@stdout) 47 | expect(val).to eq( 48 | "hello" 49 | ) 50 | end 51 | end 52 | 53 | # 54 | 55 | describe "#stderr" do 56 | it "should default to using .stderr" do 57 | expect(described_class.new.instance_variable_get(:@stderr)).to eq( 58 | described_class.stderr 59 | ) 60 | end 61 | 62 | it "should be overrideable" do 63 | val = described_class.new(nil, nil, "world"). 64 | instance_variable_get(:@stderr) 65 | expect(val).to eq( 66 | "world" 67 | ) 68 | end 69 | end 70 | 71 | # 72 | 73 | describe "#progress_bar" do 74 | subject do 75 | log :stderr, { 76 | "progress" => "world", 77 | "id" => "hello" 78 | } 79 | end 80 | 81 | # 82 | 83 | it "should prefix with the id and then message" do 84 | expect(Simple::Ansi.strip(subject).strip).to eq( 85 | "hello: world" 86 | ) 87 | end 88 | 89 | # 90 | 91 | context "when no ID is present" do 92 | subject do 93 | log :stdout, { 94 | "progress" => "world" 95 | } 96 | end 97 | 98 | it "should not log" do 99 | expect(subject).to( 100 | be_empty 101 | ) 102 | end 103 | end 104 | end 105 | 106 | # 107 | 108 | describe "#api" do 109 | context "when it's a stream" do 110 | subject do 111 | log :stdout, { 112 | "stream" => "hello\nworld" 113 | } 114 | end 115 | 116 | # 117 | 118 | it "should output what it gets" do 119 | expect(subject).to eq( 120 | "hello\nworld\n" 121 | ) 122 | end 123 | end 124 | 125 | # 126 | 127 | context "when a message is not handled" do 128 | subject do 129 | log :both, { 130 | "unknown" => "hello" 131 | } 132 | end 133 | 134 | # 135 | 136 | it "should spit out the inspect of the message" do 137 | expect(subject[:stdout].strip).to eq({ 138 | "unknown" => "hello" 139 | }.to_json) 140 | end 141 | 142 | # 143 | 144 | it "should log an error message" do 145 | expect(subject[:stderr]).not_to( 146 | be_empty 147 | ) 148 | end 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/notify_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Notify do 7 | include_context :repos 8 | 9 | # 10 | 11 | describe "#alias" do 12 | it "should have some color" do 13 | capture = capture_io do 14 | subject.alias( 15 | mocked_repo.to_scratch 16 | ) 17 | end 18 | 19 | expect(Simple::Ansi.has?(capture[:stderr])).to eq( 20 | true 21 | ) 22 | end 23 | end 24 | 25 | # 26 | 27 | describe "#build" do 28 | it "should output the user, tag and repo" do 29 | capture = capture_io do 30 | subject.build( 31 | mocked_repo.to_repo 32 | ) 33 | end 34 | 35 | expect(capture).to include({ 36 | :stderr => %r!building:[:a-z\-_\s]+/default:latest!i 37 | }) 38 | end 39 | 40 | # 41 | 42 | context "(rootfs: true)" do 43 | it "should output a rootfs image if told to" do 44 | capture = capture_io do 45 | subject.build(mocked_repo.to_repo, { 46 | rootfs: true 47 | }) 48 | end 49 | 50 | # 51 | 52 | expect(capture).to include({ 53 | :stderr => %r!building[:a-z\s]+/rootfs:default!i 54 | }) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Parser do 7 | include_context :repos 8 | 9 | # 10 | 11 | subject do 12 | described_class 13 | end 14 | 15 | # 16 | 17 | before do 18 | mocked_repo.init({ 19 | :type => :normal 20 | }) 21 | end 22 | 23 | # 24 | 25 | describe "#parse" do 26 | it "should output a set" do 27 | expect(subject.new.parse).to be_a( 28 | Array 29 | ) 30 | end 31 | 32 | # 33 | 34 | context "when given a bad identifier" do 35 | it "should throw" do 36 | expect { subject.new(["invalid/user/repo:tag"]).parse }.to raise_error( 37 | Docker::Template::Error::BadRepoName 38 | ) 39 | end 40 | 41 | # 42 | 43 | it "should throw" do 44 | expect { subject.new(["user/repo:tag:double_tag"]).parse }.to raise_error( 45 | Docker::Template::Error::BadRepoName 46 | ) 47 | end 48 | end 49 | end 50 | 51 | # 52 | 53 | describe ".to_repo_hash" do 54 | context "when given a valid identifier" do 55 | specify do 56 | expect(subject.send(:to_repo_hash, "repo:tag")).to \ 57 | include({ 58 | "name" => "repo" 59 | }) 60 | end 61 | 62 | # 63 | 64 | specify do 65 | expect(subject.send(:to_repo_hash, "repo:tag")).to \ 66 | include({ 67 | "tag" => "tag" 68 | }) 69 | end 70 | 71 | # 72 | 73 | specify do 74 | # user/repo:tag 75 | expect(subject.send(:to_repo_hash, "user/repo:tag")).to \ 76 | include({ 77 | "user" => "user" 78 | }) 79 | end 80 | 81 | # 82 | 83 | specify do 84 | expect(subject.send(:to_repo_hash, "user/repo:tag")).to \ 85 | include({ 86 | "name" => "repo" 87 | }) 88 | end 89 | 90 | # 91 | 92 | specify do 93 | expect(subject.send(:to_repo_hash, "user/repo:tag")).to \ 94 | include({ 95 | "tag" => "tag" 96 | }) 97 | end 98 | 99 | # 100 | 101 | specify do 102 | expect(subject.send(:to_repo_hash, "user/repo")).to \ 103 | include({ 104 | "user" => "user" 105 | }) 106 | end 107 | 108 | # 109 | 110 | specify do 111 | expect(subject.send(:to_repo_hash, "user/repo")).to \ 112 | include({ 113 | "name" => "repo" 114 | }) 115 | end 116 | 117 | # 118 | 119 | specify do 120 | expect(subject.send(:to_repo_hash, "repo")).to \ 121 | include({ 122 | "name" => "repo" 123 | }) 124 | end 125 | 126 | # 127 | 128 | context do 129 | before do 130 | mocked_repo.with_opts({ 131 | :tags => { 132 | "latest" => "normal" 133 | } 134 | }) 135 | end 136 | 137 | # 138 | 139 | it "should output Templates" do 140 | expect(subject.new(%w(default)).parse.first).to be_a( 141 | Docker::Template::Repo 142 | ) 143 | end 144 | end 145 | end 146 | end 147 | 148 | # 149 | 150 | describe ".full_name?" do 151 | context "when given repo/image:tag" do 152 | it "should return true" do 153 | expect(subject.full_name?("repo/image:tag")).to eq( 154 | true 155 | ) 156 | end 157 | end 158 | 159 | # 160 | 161 | context "when given repo/image" do 162 | it "should return true" do 163 | expect(subject.full_name?("repo/image")).to eq( 164 | true 165 | ) 166 | end 167 | end 168 | 169 | # 170 | 171 | context "when given repo:tag" do 172 | it "should return true" do 173 | expect(subject.full_name?("repo:tag")).to eq( 174 | true 175 | ) 176 | end 177 | end 178 | 179 | # 180 | 181 | context "when given just a user/repo" do 182 | it "should return false" do 183 | expect(subject.full_name?("nothing")).to eq( 184 | false 185 | ) 186 | end 187 | end 188 | end 189 | 190 | # 191 | 192 | describe "#all" do 193 | context "when given raw repos" do 194 | it "should return those" do 195 | expect(described_class.new(%w(hello)).all).to eq %w( 196 | hello 197 | ) 198 | end 199 | end 200 | 201 | # 202 | 203 | context "when it's project" do 204 | before do 205 | allow(Docker::Template).to receive(:project?).and_return( 206 | true 207 | ) 208 | end 209 | 210 | # 211 | 212 | subject do 213 | described_class.new 214 | end 215 | 216 | # 217 | 218 | it "should return the single root" do 219 | expect(subject.all.size).to eq 1 220 | expect(subject.all).to include( 221 | Docker::Template.root.basename.to_s 222 | ) 223 | end 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template/repo_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template::Repo do 7 | include_context :repos 8 | 9 | # 10 | 11 | subject do 12 | mocked_repo.to_repo 13 | end 14 | 15 | # 16 | 17 | it { is_expected.to respond_to :name } 18 | it { is_expected.to respond_to :tag } 19 | it { is_expected.to respond_to :type } 20 | it { is_expected.to respond_to :user } 21 | it { is_expected.to respond_to :to_h } 22 | 23 | # 24 | 25 | describe "#initialize" do 26 | context "when repo does not exist" do 27 | it "should throw" do 28 | expect { mocked_repo.empty.to_repo }.to raise_error( 29 | Docker::Template::Error::RepoNotFound 30 | ) 31 | end 32 | end 33 | end 34 | 35 | # 36 | 37 | describe "#aliased" do 38 | before do 39 | mocked_repo.add_alias :world, :tag => :hello 40 | mocked_repo.add_alias :person, :tag => "randon/image:name" 41 | mocked_repo.add_alias :local, :tag => "default:tag" 42 | mocked_repo.add_tag :hello, :group => :world 43 | mocked_repo.with_repo_init({ 44 | :tag => :world 45 | }) 46 | end 47 | 48 | it "should pull out the aliased repo" do 49 | expect(mocked_repo.to_repo.aliased.tag).to eq( 50 | "hello" 51 | ) 52 | end 53 | 54 | # 55 | 56 | context "when given a non-local full name alias" do 57 | before do 58 | mocked_repo.with_repo_init({ 59 | :tag => :person 60 | }) 61 | end 62 | 63 | # 64 | 65 | it "should not throw" do 66 | expect { mocked_repo.to_repo.aliased }.not_to( 67 | raise_error 68 | ) 69 | end 70 | end 71 | 72 | # 73 | 74 | context "when given a local full name alias" do 75 | before do 76 | mocked_repo.with_repo_init({ 77 | :tag => :local 78 | }) 79 | end 80 | 81 | # 82 | 83 | it "should return the local repo" do 84 | expect(mocked_repo.to_repo.aliased).to be_a( 85 | Docker::Template::Repo 86 | ) 87 | end 88 | end 89 | end 90 | 91 | # 92 | 93 | describe "#to_s" do 94 | context "when no type or type = :image" do 95 | context "without a user" do 96 | before do 97 | mocked_repo.with_opts({ 98 | "user" => "hello" 99 | }) 100 | end 101 | 102 | # 103 | 104 | it "should use the default user" do 105 | expect(mocked_repo.to_repo.to_s).to match( 106 | %r!\Ahello/[a-z]+:[a-z]+\Z! 107 | ) 108 | end 109 | end 110 | 111 | # 112 | 113 | context "without a tag" do 114 | before do 115 | mocked_repo.with_opts({ 116 | "tag" => "hello" 117 | }) 118 | end 119 | 120 | # 121 | 122 | it "should use the default tag" do 123 | expect(mocked_repo.to_repo.to_s).to match( 124 | %r!\A[a-z\-_]+/[a-z]+:hello! 125 | ) 126 | end 127 | end 128 | end 129 | 130 | context "when rootfs: true" do 131 | it "should use the repo name as the tag" do 132 | prefix = Docker::Template::Meta::DEFAULTS["local_prefix"] 133 | expect(mocked_repo.to_repo.to_s(rootfs: true)).to eq( 134 | "#{prefix}/rootfs:default" 135 | ) 136 | end 137 | end 138 | end 139 | 140 | # 141 | 142 | describe "#copy_dir" do 143 | it "should be a pathname" do 144 | expect(mocked_repo.to_repo.copy_dir).to be_a( 145 | Pathutil 146 | ) 147 | end 148 | 149 | # 150 | 151 | context "(*)" do 152 | it "should join arguments sent" do 153 | expect(mocked_repo.to_repo.copy_dir("world").basename.to_s).to eq( 154 | "world" 155 | ) 156 | end 157 | end 158 | end 159 | 160 | # 161 | 162 | describe "#root" do 163 | it "should put me into pry" do 164 | expect(mocked_repo.to_repo.root.relative_path_from(Docker::Template.root).to_s).to eq( 165 | "repos/default" 166 | ) 167 | end 168 | end 169 | 170 | # 171 | 172 | describe "#to_tag_h" do 173 | it "should include user/repo" do 174 | expect(mocked_repo.to_repo.to_tag_h).to \ 175 | include({ 176 | "repo" => match(%r!\A[a-z\-_]+/default!) 177 | }) 178 | end 179 | 180 | # 181 | 182 | it "should include a tag" do 183 | expect(mocked_repo.to_repo.to_tag_h).to \ 184 | include({ 185 | "tag" => "latest" 186 | }) 187 | end 188 | end 189 | 190 | # 191 | 192 | describe "#to_rootfs_h" do 193 | before do 194 | mocked_repo.init({ 195 | :type => :scratch 196 | }) 197 | end 198 | 199 | # 200 | 201 | it "should include prefix/rootfs" do 202 | prefix = Docker::Template::Meta::DEFAULTS["local_prefix"] 203 | expect(mocked_repo.to_repo.to_rootfs_h).to include({ 204 | "repo" => match(%r!\A#{Regexp.escape(prefix)}/rootfs!) 205 | }) 206 | end 207 | 208 | # 209 | 210 | it "should include a tag" do 211 | expect(mocked_repo.to_repo.to_rootfs_h).to \ 212 | include({ 213 | "tag" => "default" 214 | }) 215 | end 216 | end 217 | 218 | # 219 | 220 | describe "#tmpdir" do 221 | it "should be a pathname" do 222 | expect(mocked_repo.to_repo.tmpdir.tap(&:rm_rf)).to be_a( 223 | Pathutil 224 | ) 225 | end 226 | 227 | # 228 | 229 | it "should exist" do 230 | dir = mocked_repo.to_repo.tmpdir 231 | expect(dir).to exist 232 | dir.rm_rf 233 | end 234 | 235 | # 236 | 237 | context "(*prefixes)" do 238 | it "should add those prefixes" do 239 | expect(mocked_repo.to_repo.tmpdir("hello").tap(&:rm_rf).to_s).to match( 240 | %r!-hello-! 241 | ) 242 | end 243 | end 244 | end 245 | 246 | # 247 | 248 | describe "#tmpfile" do 249 | it "should be a Pathutil" do 250 | expect(mocked_repo.to_repo.tmpfile.tap(&:rm_rf)).to be_a( 251 | Pathutil 252 | ) 253 | end 254 | 255 | # 256 | 257 | it "should exist" do 258 | file = mocked_repo.to_repo.tmpfile 259 | expect(file).to exist 260 | file.rm_rf 261 | end 262 | 263 | # 264 | 265 | context "(*prefixes)" do 266 | it "should add those prefixes" do 267 | expect(mocked_repo.to_repo.tmpfile("hello").tap(&:rm_rf).to_s).to match( 268 | %r!-hello-! 269 | ) 270 | end 271 | end 272 | end 273 | 274 | # 275 | 276 | describe "#to_repos" do 277 | context do 278 | before do 279 | mocked_repo.with_opts({ 280 | "tags" => { 281 | "hello" => "world", 282 | "world" => "hello" 283 | } 284 | }) 285 | end 286 | 287 | # 288 | 289 | it "should pull all tags as individual repos" do 290 | expect(mocked_repo.to_repo.to_repos.size).to eq( 291 | 2 292 | ) 293 | end 294 | end 295 | 296 | # 297 | 298 | context do 299 | before do 300 | mocked_repo.with_repo_init({ 301 | "tag" => "default" 302 | }) 303 | end 304 | 305 | # 306 | 307 | it "should return all repos" do 308 | expect(mocked_repo.to_repo.to_repos.first).to be_a( 309 | Docker::Template::Repo 310 | ) 311 | end 312 | end 313 | 314 | # 315 | 316 | context "when a tag is given" do 317 | before do 318 | mocked_repo.with_repo_init({ 319 | "tag" => "default" 320 | }) 321 | end 322 | 323 | # 324 | 325 | it "should only return the current repo" do 326 | expect(mocked_repo.to_repo.to_repos.size).to eq( 327 | 1 328 | ) 329 | end 330 | end 331 | 332 | context "when the template is a project", :type => :project do 333 | before do 334 | allow(Docker::Template).to receive(:project?).and_return( 335 | true 336 | ) 337 | end 338 | 339 | # 340 | 341 | it "should return itself as a set" do 342 | repo = mocked_repo.to_repo 343 | expect(repo.to_repos.size).to eq 1 344 | expect(repo.to_repos).to include( 345 | repo 346 | ) 347 | end 348 | end 349 | end 350 | 351 | # 352 | 353 | describe "#meta" do 354 | it "should be a Meta" do 355 | expect(mocked_repo.to_repo.meta).to be_a( 356 | Docker::Template::Meta 357 | ) 358 | end 359 | end 360 | 361 | # 362 | 363 | describe "#to_env" do 364 | it "should return a hash to you" do 365 | expect(mocked_repo.to_repo.to_env).to be_a( 366 | Docker::Template::Meta 367 | ) 368 | end 369 | 370 | # 371 | 372 | context "(tar_gz: val)" do 373 | let :result do 374 | mocked_repo.to_repo.to_env({ 375 | :tar_gz => "val" 376 | }) 377 | end 378 | 379 | it "should include the tar_gz" do 380 | expect(result[:all]).to include({ 381 | "TAR_GZ" => "val" 382 | }) 383 | end 384 | end 385 | 386 | # 387 | 388 | context "copy_dir: val" do 389 | let :result do 390 | mocked_repo.to_repo.to_env({ 391 | :copy_dir => "val" 392 | }) 393 | end 394 | 395 | it "should include the copy_dir" do 396 | expect(result[:all]).to include({ 397 | "COPY_DIR" => "val" 398 | }) 399 | end 400 | end 401 | end 402 | end 403 | -------------------------------------------------------------------------------- /spec/tests/lib/docker/template_spec.rb: -------------------------------------------------------------------------------- 1 | # Frozen-string-literal: true 2 | # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License 3 | # Encoding: utf-8 4 | 5 | require "rspec/helper" 6 | describe Docker::Template do 7 | it { is_expected.to respond_to :gem_root } 8 | it { is_expected.to respond_to :template_root } 9 | it { is_expected.to respond_to :root } 10 | it { is_expected.to respond_to :get } 11 | 12 | let :template do 13 | described_class 14 | end 15 | 16 | # 17 | 18 | [:gem_root, :template_root, :root].each do |val| 19 | describe "##{val}" do 20 | it "should be a Pathame" do 21 | expect(template.send(val)).to be_a( 22 | Pathutil 23 | ) 24 | end 25 | end 26 | end 27 | 28 | # 29 | 30 | describe "#project?" do 31 | include_context :repos 32 | context "when laid out as a project repository", :type => :project do 33 | it "should return true" do 34 | expect(template.project?).to eq( 35 | true 36 | ) 37 | end 38 | end 39 | 40 | # 41 | 42 | context "when it's not laid out as project" do 43 | it "should return false" do 44 | expect(template.project?).to( 45 | be_falsey 46 | ) 47 | end 48 | end 49 | end 50 | 51 | # 52 | 53 | describe "#get" do 54 | context "when no data is given" do 55 | it "should still return a string" do 56 | expect(template.get(:rootfs)).to be_a( 57 | String 58 | ) 59 | end 60 | end 61 | 62 | # 63 | 64 | context "when data is given" do 65 | it "should parse that data with ERB" do 66 | expect(template.get(:rootfs, :rootfs_base_img => "hello world")).to start_with( 67 | "FROM hello world\n" 68 | ) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /templates/rootfs.erb: -------------------------------------------------------------------------------- 1 | FROM <%= @rootfs_base_img %> 2 | COPY copy / 3 | ENTRYPOINT [ \ 4 | "/usr/local/bin/mkimg" \ 5 | ] 6 | -------------------------------------------------------------------------------- /templates/rootfs/alpine.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ "$DEBUG" ] && set -x 3 | set -e 4 | 5 | ROOTFS=$(mktemp -d) 6 | TMPDIR=$(mktemp -d) 7 | 8 | MIRROR_HOST="<%= @meta.mirror_host %>" 9 | [ "$MIRROR_HOST" ] || MIRROR_HOST="mirror.envygeeks.io" 10 | MIRRORS="\nhttp://$MIRROR_HOST/alpine/<%= @meta.release =~ /^[\d,\.]+$/ ? "v" : "" %><%= @meta.release %>/main" 11 | MIRRORS=$MIRRORS"\n@community http://$MIRROR_HOST/alpine/<%= @meta.release =~ /^[\d,\.]+$/ ? "v" : "" %><%= @meta.release %>/community" 12 | MIRRORS=$MIRRORS"\n@communityEdge http://$MIRROR_HOST/alpine/edge/community" 13 | MIRRORS=$MIRRORS"\n@testing http://$MIRROR_HOST/alpine/edge/testing" 14 | MIRRORS=$MIRRORS"\n@edge http://$MIRROR_HOST/alpine/edge/main" 15 | RELEASE_URL="http://$MIRROR_HOST/alpine/<%= @meta.release \ 16 | =~ /^[\d,\.]+$/ ? "v" : "" %><%= @meta.release %>/main" 17 | 18 | cd $TMPDIR 19 | mkdir -p $ROOTFS/etc 20 | apk update --repository=$RELEASE_URL 21 | apk fetch --stdout --repository=$RELEASE_URL alpine-keys | \ 22 | tar -xvzC $ROOTFS etc/ 23 | 24 | # -- 25 | 26 | apk --initdb --root=$ROOTFS --repository=$RELEASE_URL --update-cache add \ 27 | apk-tools libc-utils alpine-baselayout alpine-keys busybox musl \ 28 | busybox-suid alpine-conf 29 | 30 | # -- 31 | 32 | apk --keys-dir=$ROOTFS/etc/apk/keys --root=$ROOTFS --repository=$RELEASE_URL \ 33 | fetch --stdout alpine-base | \ 34 | tar -xvz -C "$ROOTFS" etc/ 35 | 36 | # -- 37 | 38 | cd ~> /dev/null 39 | cp -R $COPY_DIR/* $ROOTFS 2> /dev/null || true 40 | 41 | # -- 42 | 43 | cp /etc/resolv.conf $ROOTFS/etc/resolv.conf 44 | printf "$MIRRORS" > $ROOTFS/etc/apk/repositories 45 | cp /etc/hosts $ROOTFS/etc/hosts 46 | 47 | apk --root=$ROOTFS update 48 | apk --root=$ROOTFS add <%= @meta.packages %> 49 | apk del --root=$ROOTFS <%= @meta.package_cleanup %> 50 | apk del --root=$ROOTFS <%= @meta.package_deep_cleanup %> 51 | mv $ROOTFS/var/run/* $ROOTFS/run 2> /dev/null || true 52 | mv $ROOTFS/var/lock $ROOTFS/run 2> /dev/null || true 53 | rm -rf $ROOTFS/var/run $ROOTFS/var/lock 54 | ln -sf /run/lock $ROOTFS/var/lock 55 | ln -sf /run $ROOTFS/var/run 56 | 57 | # -- 58 | 59 | <% if @meta.helpers? %> 60 | mkdir -p /usr/src 61 | cd /usr/src 62 | 63 | apk update 64 | apk add ca-certificates git 65 | git clone --verbose https://github.com/envygeeks/docker-helper.git 66 | cp -R docker-helper/src/* $ROOTFS/ 67 | <% end %> 68 | 69 | # -- 70 | 71 | $ROOTFS/usr/bin/cleanup $ROOTFS || true 72 | rm -rf $ROOTFS/etc/hosts 73 | rm -rf $ROOTFS/etc/resolv.conf 74 | tar -zf $TAR_GZ --numeric-owner -C $ROOTFS -c . 75 | -------------------------------------------------------------------------------- /templates/rootfs/ubuntu.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ "$DEBUG" ] && set -x 3 | set -e 4 | 5 | ROOTFS=$(mktemp -d) 6 | MIRROR_HOST="<%= @meta.mirror_host %>" 7 | [ "$MIRROR_HOST" ] || MIRROR_HOST="archive.ubuntu.com" 8 | PARTNER_URL="https://mirror.envygeeks.io/core-ubuntu" 9 | TMP=$(mktemp -d) 10 | 11 | # -- 12 | 13 | cd $TMP 14 | apk update 15 | apk add wget gnupg ca-certificates \ 16 | gawk sed 17 | 18 | # -- 19 | 20 | gpg --keyserver keyserver.ubuntu.com --recv-keys 1A5D6C4C7DB87C81 21 | wget $PARTNER_URL/<%= @meta.release %>/SHA256SUMS.gpg 22 | wget $PARTNER_URL/<%= @meta.release %>/SHA256SUMS 23 | gpg --verify SHA256SUMS.gpg SHA256SUMS 24 | 25 | cat SHA256SUMS 26 | sha=$(cat SHA256SUMS | grep "amd64" | awk -F' *' '{ print $1 }') 27 | img=$(cat SHA256SUMS | grep "amd64" | awk -F' *' '{ print $2 }' \ 28 | | sed -r 's/^\*//') 29 | 30 | wget --progress=bar "$PARTNER_URL/<%= @meta.release %>/$img" 31 | if [ "$(sha256sum $img | awk '{ print $1 }')" != "$sha" ]; then 32 | echo "Bailing, the SHA256sum did not match." 33 | fi 34 | 35 | # -- 36 | 37 | tar xzf $img -C $ROOTFS 38 | cd -> /dev/null 39 | 40 | # -- 41 | 42 | rm -rf $ROOTFS/etc/hosts 43 | rm -rf $ROOTFS/etc/resolv.conf 44 | cp /etc/resolv.conf $ROOTFS/etc/resolv.conf 45 | cp /etc/hosts $ROOTFS/etc/hosts 46 | 47 | # -- 48 | 49 | chroot "$ROOTFS" sh -ec "dpkg-divert --local --rename --add /sbin/initctl" 50 | chroot "$ROOTFS" sh -ec "dpkg-divert --local --rename --add /usr/sbin/update-rc.d" 51 | ln -s /bin/true $ROOTFS/usr/sbin/update-rc.d 52 | ln -s /bin/true $ROOTFS/sbin/initctl 53 | 54 | # -- 55 | 56 | sed -i 's/^#\s*\(deb.*universe\)$/\1/g' $ROOTFS/etc/apt/sources.list 57 | sed -i "s/archive\.ubuntu\.com/$MIRROR_HOST/g" $ROOTFS/etc/apt/sources.list 58 | chroot "$ROOTFS" sh -ec 'echo "debconf debconf/frontend select Noninteractive" | debconf-set-selections' 59 | echo 'Dpkg::Options { "--force-confdef"; "--force-confold"; }' > $ROOTFS/etc/apt/apt.conf.d/03confdef 60 | echo 'APT::Get::Install-Recommends "false"; APT::Get::Install-Suggests "false";' > \ 61 | $ROOTFS/etc/apt/apt.conf.d/00norecommends 62 | 63 | # -- 64 | 65 | echo "exit 101" > $ROOTFS/usr/sbin/policy-rc.d policy-rc.d 66 | echo 'APT::Get::Assume-Yes "true";' > $ROOTFS/etc/apt/apt.conf.d/01yes 67 | echo 'Apt::Get::Purge "true";' > $ROOTFS/etc/apt/apt.conf.d/02purge 68 | sed -ri '/^(deb-src\s+|$|#)/d' $ROOTFS/etc/apt/sources.list 69 | chmod uog+x $ROOTFS/usr/sbin/policy-rc.d 70 | 71 | chroot "$ROOTFS" sh -ec "{ 72 | apt-get update 73 | apt-get install locales deborphan <%= @meta.packages %> 74 | echo 'Yes, do as I say!' | SUDO_FORCE_REMOVE=yes apt-get autoremove -f <%= @meta.package_cleanup %> 75 | echo 'Yes, do as I say!' | SUDO_FORCE_REMOVE=yes apt-get autoremove -f <%= @meta.package_deep_cleanup %> 76 | apt-get autoremove \$(deborphan --guess-all) deborphan 77 | apt-get autoremove 78 | apt-get autoclean 79 | apt-get clean 80 | 81 | rm -rf /tmp/remove 82 | }" 83 | 84 | # -- 85 | 86 | <% if @meta.helpers? %> 87 | mkdir -p /usr/src 88 | cd /usr/src 89 | 90 | apk add ca-certificates git 91 | git clone --verbose https://github.com/envygeeks/docker-helper.git 92 | cp -R docker-helper/src/* $ROOTFS/ 93 | <% end %> 94 | 95 | # -- 96 | 97 | cp -R $COPY_DIR/* $ROOTFS 98 | cleanup $ROOTFS 99 | 100 | # -- 101 | 102 | rm -rf $ROOTFS/etc/hosts 103 | rm -rf $ROOTFS/etc/resolv.conf 104 | tar -zf $TAR_GZ --numeric-owner -C $ROOTFS -c . 105 | -------------------------------------------------------------------------------- /templates/scratch.erb: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | MAINTAINER <%= @maintainer %> 3 | ADD <%= @tar_gz %> / 4 | <% if @entrypoint %> 5 | ENTRYPOINT ["<%= @entrypoint %>"] 6 | <% end %> 7 | --------------------------------------------------------------------------------