├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── configuration-example.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── dependabot.yml └── workflows │ ├── expect.exp │ ├── goreleaser.yml │ └── pygmy.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── builds └── .gitkeep ├── cmd ├── addkey.go ├── clean.go ├── completion.go ├── down.go ├── export.go ├── restart.go ├── root.go ├── status.go ├── stop.go ├── up.go ├── update.go └── version.go ├── docs ├── connect_to_mysql_from_external.md ├── customisation │ └── introduction.md ├── drupal_site_containers.md ├── images │ ├── Sequel_Pro.png │ └── Statistics_Report_for_HAProxy_on_c06d7fc60984.jpg ├── index.md ├── installation.md ├── local_docker_development.md ├── map_addtitional_ports.md ├── readthedocs.yml ├── requirements.txt ├── ssh_agent.md ├── troubleshooting.md ├── update.md └── usage.md ├── examples ├── pygmy.basic.yml ├── pygmy.complex.yml ├── pygmy.noresolv.yml └── pygmy.overrides.yml ├── external └── docker │ ├── commands │ ├── addkey.go │ ├── clean.go │ ├── down.go │ ├── export.go │ ├── restart.go │ ├── status.go │ ├── stop.go │ ├── up.go │ ├── update.go │ └── version.go │ └── setup │ ├── dryrun.go │ ├── network.go │ ├── service.go │ ├── setup.go │ ├── setup_test.go │ ├── types.go │ ├── utils.go │ └── volume.go ├── go.mod ├── go.sum ├── internal ├── runtime │ ├── docker │ │ ├── docker.go │ │ ├── internals │ │ │ ├── client.go │ │ │ ├── containers │ │ │ │ ├── container.go │ │ │ │ └── container_test.go │ │ │ ├── context │ │ │ │ ├── context.go │ │ │ │ └── context_test.go │ │ │ ├── images │ │ │ │ ├── image.go │ │ │ │ └── image_test.go │ │ │ ├── networks │ │ │ │ ├── network.go │ │ │ │ └── network_test.go │ │ │ └── volumes │ │ │ │ ├── volume.go │ │ │ │ └── volume_test.go │ │ ├── types.go │ │ └── utils.go │ ├── podman │ │ └── .gitkeep │ └── runtime.go ├── service │ └── docker │ │ ├── dnsmasq │ │ ├── dnsmasq.go │ │ └── dnsmasq_test.go │ │ ├── haproxy │ │ ├── haproxy.go │ │ └── haproxy_test.go │ │ ├── mailhog │ │ ├── mailhog.go │ │ └── mailhog_test.go │ │ └── ssh │ │ ├── agent │ │ ├── ssh_agent.go │ │ └── ssh_agent_test.go │ │ └── key │ │ ├── ssh_addkey.go │ │ ├── ssh_addkey_test.go │ │ └── ssh_addkey_win.go └── utils │ ├── color │ └── color.go │ ├── endpoint │ ├── endpoint.go │ └── endpoint_test.go │ ├── network │ └── docker │ │ ├── docker.go │ │ └── docker_test.go │ └── resolv │ ├── resolv.go │ ├── resolvWin.go │ └── types.go ├── main.go ├── main_test.go └── mkdocs.yml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug] describe the bug" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Output** 27 | A copy of the terminal/shell output where possible/useful. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | 32 | **Exported configuration** 33 | 34 | Run `pygmy export --output output.yml` and print the contents from `output.yml` below: 35 | 36 | ``` 37 | Exported configuration goes here. 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/configuration-example.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Configuration example 3 | about: Request an example configuration for a new container. 4 | title: Request for example pygmy ______ integration 5 | labels: configuration-request 6 | assignees: fubarhouse 7 | 8 | --- 9 | 10 | **Background** 11 | 12 | Reason for asking and what you're interested in achieving with this. This will serve as a reference to other users making similar requests if yours is more suitable. 13 | 14 | **Implementation** 15 | 16 | __Docker__ 17 | 18 | The docker command is required for this work. Please identify the full docker command in which you normally use to run the container. This is required and your request will likely be delayed without a reasonable explanation of why it is missing. 19 | 20 | ``` 21 | docker run -it --rm mbentley/cowsay holy ship! 22 | ``` 23 | 24 | __Variables__ 25 | 26 | Variables which would need injection to the container at start-up. 27 | 28 | ``` 29 | MYVAR1=hello 30 | MYVAR2=world! 31 | ``` 32 | 33 | __Volumes__ 34 | 35 | If any named volumes need to be present for this container please name them here - pygmy will create them but managing and removing them will be your responsibility. 36 | 37 | ``` 38 | - myservice_filesystem 39 | - myservice_database 40 | ``` 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] describe the feature" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **An issue for this change is required** 2 | 3 | --- 4 | 5 | **Description** 6 | 7 | Provide a description of the pull request. 8 | 9 | **Resolves** 10 | 11 | This PR resolves issue #__ 12 | 13 | **Non-standard tests for this change** 14 | 15 | If any non-standard testing is expected, please describe it here. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "19:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: github.com/spf13/cobra 11 | versions: 12 | - 1.1.2 13 | -------------------------------------------------------------------------------- /.github/workflows/expect.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect 2 | spawn ./pygmy-linux-amd64 addkey -k /home/runner/.ssh/id_pwd 3 | expect "Enter passphrase " 4 | send "passphrase\r" 5 | expect "Identity added: " 6 | spawn ./pygmy-linux-amd64 status 7 | interact 8 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | goreleaser: 15 | runs-on: ubuntu-22.04 16 | env: 17 | GO111MODULE: on 18 | steps: 19 | - 20 | name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: "0" 24 | - 25 | name: System dependencies 26 | run: sudo apt update && sudo apt install git golang -y || true 27 | - 28 | name: Set up Go 29 | uses: actions/setup-go@v4 30 | with: 31 | go-version: '1.21' 32 | - 33 | name: Fetch Dependencies 34 | run: go mod tidy && go mod vendor 35 | - 36 | name: Print version string 37 | run: go run main.go version 38 | 39 | - 40 | name: Check GoReleaser 41 | uses: goreleaser/goreleaser-action@v6 42 | with: 43 | distribution: goreleaser 44 | version: '~> v2' 45 | args: check 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | - 49 | name: Dry-Run GoReleaser 50 | uses: goreleaser/goreleaser-action@v6 51 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 52 | with: 53 | distribution: goreleaser 54 | version: '~> v2' 55 | args: release --clean --snapshot --skip=docker --skip=homebrew --skip=publish 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | - 59 | name: Run GoReleaser 60 | uses: goreleaser/goreleaser-action@v6 61 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 62 | with: 63 | distribution: goreleaser 64 | version: '~> v2' 65 | args: release --clean --skip=homebrew 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | builds/* 2 | dist/* 3 | vendor/* 4 | .idea/* 5 | 6 | external/podman/* 7 | internal/runtime/podman/* 8 | internal/service/podman/* 9 | internal/utils/network/podman/* -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | env: 3 | - GO111MODULE=on 4 | - GOPROXY=https://gocenter.io 5 | 6 | archives: 7 | - id: pygmy 8 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 9 | ids: 10 | - pygmy 11 | - id: pygmy-static 12 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}_static" 13 | ids: 14 | - pygmy-static 15 | 16 | builds: 17 | - id: pygmy 18 | env: 19 | - CGO_ENABLED=0 20 | ldflags: 21 | - -X main.Version={{.Tag}} 22 | - -X main.CommitSHA={{.FullCommit}} 23 | - -X main.BuildDate={{.CommitDate}} 24 | - -X main.GoOS={{.Os}} 25 | - -X main.GoArch={{.Arch}} 26 | goos: 27 | - linux 28 | - darwin 29 | goarch: 30 | - amd64 31 | - arm64 32 | - id: pygmy-static 33 | env: 34 | - CGO_ENABLED=0 35 | flags: 36 | - -a 37 | ldflags: 38 | - -X main.Version={{.Tag}} 39 | - -X main.CommitSHA={{.FullCommit}} 40 | - -X main.BuildDate={{.CommitDate}} 41 | - -X main.GoOS={{.Os}} 42 | - -X main.GoArch={{.Arch}} 43 | - -extldflags "-static" 44 | goos: 45 | - linux 46 | goarch: 47 | - amd64 48 | - arm64 49 | 50 | snapshot: 51 | version_template: "{{ .Version }}-SNAPSHOT-{{.ShortCommit}}" 52 | 53 | brews: 54 | - ids: 55 | - pygmy 56 | repository: 57 | owner: pygmystack 58 | name: homebrew-pygmy 59 | branch: main 60 | token: "${{ .Env.GITHUB_TOKEN }}" 61 | homepage: "https://github.com/pygmystack/pygmy" 62 | description: "amazee.io's local development helper tool" 63 | skip_upload: false 64 | test: system "#{bin}/pygmy version" 65 | install: bin.install "pygmy" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | LABEL stage=builder 3 | COPY main.go /go/src/github.com/pygmystack/pygmy/ 4 | COPY go.sum /go/src/github.com/pygmystack/pygmy/ 5 | COPY go.mod /go/src/github.com/pygmystack/pygmy/ 6 | COPY cmd/ /go/src/github.com/pygmystack/pygmy/cmd/ 7 | COPY internal/ /go/src/github.com/pygmystack/pygmy/internal/ 8 | COPY external/ /go/src/github.com/pygmystack/pygmy/external/ 9 | 10 | WORKDIR /go/src/github.com/pygmystack/pygmy/ 11 | RUN GO111MODULE=on go mod verify 12 | RUN GO111MODULE=on GOOS=linux GOARCH=386 go build -o pygmy-linux-386 . 13 | RUN GO111MODULE=on GOOS=linux GOARCH=386 CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o pygmy-linux-386-static . 14 | RUN GO111MODULE=on GOOS=linux GOARCH=arm go build -o pygmy-linux-arm . 15 | RUN GO111MODULE=on GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o pygmy-linux-arm-static . 16 | RUN GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o pygmy-linux-arm64 . 17 | RUN GO111MODULE=on GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o pygmy-linux-arm64-static . 18 | RUN GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o pygmy-linux-amd64 . 19 | RUN GO111MODULE=on GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o pygmy-linux-amd64-static . 20 | RUN GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o pygmy-darwin-amd64 . 21 | RUN GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o pygmy-darwin-arm64 . 22 | RUN GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o pygmy.exe . 23 | 24 | FROM alpine 25 | WORKDIR /app 26 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-386 . 27 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-386-static . 28 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-arm . 29 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-arm-static . 30 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-arm64 . 31 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-arm64-static . 32 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-amd64 . 33 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-linux-amd64-static . 34 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-darwin-amd64 . 35 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy-darwin-arm64 . 36 | COPY --from=builder /go/src/github.com/pygmystack/pygmy/pygmy.exe . 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | DIR := ${CURDIR} 4 | 5 | build: 6 | docker build -t pygmy . 7 | @echo "Removing binaries from previous build" 8 | docker run --rm -v $(DIR):/data pygmy sh -c 'rm -f /data/builds/pygmy*' 9 | @echo "Done" 10 | @echo "Copying binaries to build directory" 11 | docker run --rm -v $(DIR):/data pygmy sh -c 'cp pygmy* /data/builds/.' 12 | @echo "Done" 13 | @echo "Enjoy using pygmy binaries in the $(DIR)/builds directory." 14 | 15 | clean: 16 | docker image rm -f pygmy 17 | docker image prune -f --filter="label=stage=builder" 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pygmy 2 | 3 | [![Stability](https://img.shields.io/badge/stability-stable-green.svg)]() 4 | ![goreleaser](https://github.com/pygmystack/pygmy/workflows/goreleaser/badge.svg) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/pygmystack/pygmy)](https://goreportcard.com/report/github.com/pygmystack/pygmy) 6 | [![GoDoc](https://godoc.org/github.com/pygmystack/pygmy?status.svg)](https://godoc.org/github.com/pygmystack/pygmy) 7 | 8 | This is an application written in Go which is a proposed replacement for [Pygmy](https://pygmy.readthedocs.io/en/master/) 9 | currently written in Ruby. The goal is to provide a better cross-platform experience 10 | for various users running Lagoon, as well as much greater control over configuration 11 | options via YAML. 12 | 13 | Please see the existing [Pygmy documentation](https://pygmy.readthedocs.io) for more information 14 | about Pygmy as this is designed to be a drop-in replacement. 15 | 16 | ## Early testing 17 | 18 | We welcome testers of this tool. You will probably be an existing user of Pygmy who 19 | can verify the same functionality, or perhaps who has had trouble installing Pygmy in the 20 | past on Windows. 21 | 22 | ## Is Pygmy running? 23 | 24 | These instructions will currently install the new version as `pygmy` so that the 25 | old version is still available if you have installed it. With no Pygmy running, 26 | you should get "connection refused" when attempting to connect to the local amazee network. 27 | 28 | ``` 29 | curl --HEAD http://myproject.docker.amazee.io 30 | curl: (7) Failed to connect to myproject.docker.amazee.io port 80: Connection refused 31 | ``` 32 | 33 | ## Installation 34 | 35 | These instructions will build Linux, MacOS and Windows binaries of Pygmy on MacOS, 36 | and then test the MacOS version. M1 and arm64 images are available and supported 37 | 38 | ### Using Homebrew 39 | 40 | Homebrew is the recommended way to install pygmy and keep it up to date on compatible systems. 41 | 42 | **Works for**: Linux & MacOS (also WSL-based systems) 43 | 44 | ```shell 45 | brew tap pygmystack/pygmy; 46 | brew install pygmy; 47 | ``` 48 | 49 | ### Compile from source 50 | 51 | Ensure to select the correct build for your OS and architecture in the `cp` command. 52 | 53 | **Works for**: Linux, MacOS & Windows 54 | 55 | ```shell 56 | git clone https://github.com/pygmystack/pygmy.git && cd pygmy; 57 | make build; 58 | cp ./builds/pygmy-darwin /usr/local/bin/pygmy; 59 | chmod +x /usr/local/bin/pygmy; 60 | ``` 61 | 62 | Pygmy is now executable as `pygmy`. Now start Pygmy and use the new `status` command. 63 | If you still need to use the previous `pygmy`, cp the binary to a different name (e.g. pygmy-go) 64 | 65 | ### Using the AUR 66 | 67 | **Works for**: [Arch-based Linux Distributions](https://wiki.archlinux.org/title/Arch-based_distributions) (Manjaro, Elementary, ArcoLinux etc) 68 | 69 | [pygmy](https://aur.archlinux.org/packages/pygmy/), [pygmy-bin](https://aur.archlinux.org/packages/pygmy-bin/) and 70 | [pygmy-git](https://aur.archlinux.org/packages/pygmy-git/) are available via the Arch User Repository for Arch-based 71 | Linux distributions on the community stream. Unfortunately, Pygmy is not yet available via other distribution methods, 72 | so it is otherwise recommended to use homebrew to install it, download a pre-compiled binary from the releases page, or 73 | to compile from source. 74 | 75 | ```shell 76 | # Freshly compile the latest release: 77 | yay -S pygmy; 78 | # Download the latest release precompiled: 79 | yay -S pygmy-bin; 80 | # Download and compile the latest HEAD from GitHub on the main branch: 81 | yay -S pygmy-git; 82 | ``` 83 | 84 | ## Usage 85 | 86 | If you have an Amazee Lagoon project running, you can test the web address and 87 | expect a `HTTP/1.1 200 OK` response. 88 | 89 | ``` 90 | $ curl --HEAD http://myproject.docker.amazee.io 91 | HTTP/1.1 200 OK 92 | Server: openresty 93 | Content-Type: text/html; charset=UTF-8 94 | Cache-Control: must-revalidate, no-cache, private 95 | Date: Mon, 11 Nov 2019 11:19:29 GMT 96 | X-UA-Compatible: IE=edge 97 | Content-language: en 98 | X-Content-Type-Options: nosniff 99 | X-Frame-Options: SAMEORIGIN 100 | X-Drupal-Cache-Tags: config:honeypot.settings config:system.site config:user.role.anonymous http_response rendered 101 | X-Drupal-Cache-Contexts: languages:language_interface theme url.path url.query_args user.permissions user.roles:authenticated 102 | Expires: Sun, 19 Nov 1978 05:00:00 GMT 103 | Vary: 104 | X-Frame-Options: SameOrigin 105 | ``` 106 | 107 | If your project is not running you should expect a 503 response: 108 | 109 | ``` 110 | $ curl --HEAD http://FUBARNOTINDAHOUSE.docker.amazee.io 111 | HTTP/1.0 503 Service Unavailable 112 | Cache-Control: no-cache 113 | Connection: close 114 | Content-Type: text/html 115 | ``` 116 | 117 | Thanks for testing, please post issues and successes in the queue. 118 | 119 | ## Local development 120 | 121 | To run full regression tests locally, you can follow this process if you have `cmake`, `git` and `go` installed. This 122 | will prevent a significant amount of build failures and problems after committing. 123 | 124 | It will use `dind` and your local daemon to walk through several tests which should pass. 125 | 126 | 1. First clone the project: 127 | ``` 128 | git clone https://github.com/pygmystack/pygmy.git pygmy && cd pygmy 129 | ``` 130 | 2. Perform any updates as required. 131 | 3. Clean the environment. 132 | ``` 133 | go run main.go clean 134 | ``` 135 | 4. Build the project. 136 | ``` 137 | make 138 | ``` 139 | 5. Test the project prior to commiting. 140 | ``` 141 | go test -v 142 | ``` 143 | 144 | ## Releasing 145 | 146 | We use GitHub Actions for simulating the automated release tagging locally. Using [Act](https://github.com/nektos/act) locally, you can simulate this process and have the same build artifacts in your `dist` folder. 147 | This process will inject the appropriate values into the version logic. To start the process, just run `act`! 148 | -------------------------------------------------------------------------------- /builds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pygmystack/pygmy/2cb96865d6205cf331171954e73cc4310ee2f6e4/builds/.gitkeep -------------------------------------------------------------------------------- /cmd/addkey.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/pygmystack/pygmy/external/docker/setup" 26 | 27 | . "github.com/logrusorgru/aurora" 28 | "github.com/spf13/cobra" 29 | 30 | "github.com/pygmystack/pygmy/external/docker/commands" 31 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 32 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 33 | "github.com/pygmystack/pygmy/internal/utils/color" 34 | ) 35 | 36 | // addkeyCmd is the SSH key add command. 37 | var addkeyCmd = &cobra.Command{ 38 | Use: "addkey", 39 | Example: "pygmy addkey --key ~/.ssh/id_rsa", 40 | Short: "Add/re-add an SSH key to the agent", 41 | Long: `Add or re-add an SSH key to Pygmy's SSH Agent by specifying the path to the private key.`, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | 44 | cli, ctx, err := internals.NewClient() 45 | if err != nil { 46 | fmt.Println(err) 47 | } 48 | 49 | Key, _ := cmd.Flags().GetString("key") 50 | var Keys []setup.Key 51 | 52 | if Key != "" { 53 | thisKey := setup.Key{ 54 | Path: Key, 55 | } 56 | Keys = append(Keys, thisKey) 57 | } else { 58 | if len(Keys) == 0 { 59 | setup.Setup(ctx, cli, &c) 60 | Keys = c.Keys 61 | } 62 | } 63 | 64 | for _, k := range Keys { 65 | if e := commands.SshKeyAdd(c, k.Path); e != nil { 66 | color.Print(Red(fmt.Sprintf("%v\n", e))) 67 | } 68 | } 69 | 70 | for _, s := range c.SortedServices { 71 | service := c.Services[s] 72 | purpose, _ := service.GetFieldString(ctx, cli, "purpose") 73 | if purpose == "sshagent" { 74 | name, _ := service.GetFieldString(ctx, cli, "name") 75 | d, _ := containers.Exec(ctx, cli, name, "ssh-add -l") 76 | fmt.Println(string(d)) 77 | } 78 | } 79 | 80 | }, 81 | } 82 | 83 | func init() { 84 | rootCmd.AddCommand(addkeyCmd) 85 | addkeyCmd.Flags().StringP("key", "k", "", "Path of SSH key to add") 86 | } 87 | -------------------------------------------------------------------------------- /cmd/clean.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/spf13/cobra" 26 | 27 | "github.com/pygmystack/pygmy/external/docker/commands" 28 | ) 29 | 30 | // stopCmd represents the stop command 31 | var cleanCmd = &cobra.Command{ 32 | Use: "clean", 33 | Example: "pygmy clean", 34 | Short: "Stop and remove all pygmy services regardless of state", 35 | Long: `Useful for debugging or system cleaning, this command will 36 | remove all pygmy containers but leave the images in-tact. 37 | 38 | This command does not check if the containers are running 39 | because other checks do for speed convenience.`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | 42 | err := commands.Clean(c) 43 | if err != nil { 44 | fmt.Println(err) 45 | } 46 | 47 | }, 48 | } 49 | 50 | func init() { 51 | 52 | rootCmd.AddCommand(cleanCmd) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "os" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | // completionCmd represents the completion command 30 | var completionCmd = &cobra.Command{ 31 | Use: "completion [bash|zsh|fish|powershell]", 32 | Short: "Generate completion script", 33 | Long: `To load completions: 34 | 35 | Bash: 36 | 37 | $ source <(pygmy completion bash) 38 | 39 | # To load completions for each session, execute once: 40 | # Linux: 41 | $ pygmy completion bash > /etc/bash_completion.d/pygmy 42 | # macOS: 43 | $ pygmy completion bash > /usr/local/etc/bash_completion.d/pygmy 44 | 45 | Zsh: 46 | 47 | # If shell completion is not already enabled in your environment, 48 | # you will need to enable it. You can execute the following once: 49 | 50 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 51 | 52 | # To load completions for each session, execute once: 53 | $ pygmy completion zsh > "${fpath[1]}/pygmy" 54 | 55 | # You will need to start a new shell for this setup to take effect. 56 | 57 | fish: 58 | 59 | $ pygmy completion fish | source 60 | 61 | # To load completions for each session, execute once: 62 | $ pygmy completion fish > ~/.config/fish/completions/pygmy.fish 63 | 64 | PowerShell: 65 | 66 | PS> pygmy completion powershell | Out-String | Invoke-Expression 67 | 68 | # To load completions for every new session, run: 69 | PS> pygmy completion powershell > pygmy.ps1 70 | # and source this file from your PowerShell profile. 71 | `, 72 | DisableFlagsInUseLine: true, 73 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 74 | Args: cobra.MatchAll(cobra.ExactArgs(1)), 75 | Run: func(cmd *cobra.Command, args []string) { 76 | switch args[0] { 77 | case "bash": 78 | _ = cmd.Root().GenBashCompletion(os.Stdout) 79 | case "zsh": 80 | _ = cmd.Root().GenZshCompletion(os.Stdout) 81 | case "fish": 82 | _ = cmd.Root().GenFishCompletion(os.Stdout, true) 83 | case "powershell": 84 | _ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) 85 | } 86 | }, 87 | } 88 | 89 | func init() { 90 | rootCmd.AddCommand(completionCmd) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/down.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/spf13/cobra" 26 | 27 | "github.com/pygmystack/pygmy/external/docker/commands" 28 | ) 29 | 30 | // downCmd represents the down command 31 | var downCmd = &cobra.Command{ 32 | Use: "down", 33 | Example: "pygmy down", 34 | Short: "Stop and remove all pygmy services", 35 | Long: `Check if any pygmy containers are running and removes 36 | then if they are, it will attempt to remove any 37 | services which are not running.`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | 40 | err := commands.Down(c) 41 | if err != nil { 42 | fmt.Println(err) 43 | } 44 | 45 | }, 46 | } 47 | 48 | func init() { 49 | 50 | rootCmd.AddCommand(downCmd) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cmd/export.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | 27 | "github.com/mitchellh/go-homedir" 28 | "github.com/spf13/cobra" 29 | 30 | "github.com/pygmystack/pygmy/external/docker/commands" 31 | ) 32 | 33 | var exportPath string 34 | 35 | // exportCmd represents the status command 36 | var exportCmd = &cobra.Command{ 37 | Use: "export", 38 | Example: "pygmy export --config /path/to/input --output /path/to/output", 39 | Short: "Export validated configuration to a given path", 40 | Long: `Export configuration which has validated into a specified path`, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | 43 | err := commands.Export(c, exportPath) 44 | if err != nil { 45 | fmt.Println(err) 46 | } 47 | 48 | }, 49 | } 50 | 51 | func init() { 52 | 53 | rootCmd.AddCommand(exportCmd) 54 | 55 | homedir, _ := homedir.Dir() 56 | exportPath = fmt.Sprintf("%v%v.pygmy.yml", homedir, string(os.PathSeparator)) 57 | 58 | exportCmd.Flags().StringVarP(&exportPath, "output", "o", exportPath, "Path to exported configuration to be written to") 59 | 60 | } 61 | -------------------------------------------------------------------------------- /cmd/restart.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/pygmystack/pygmy/external/docker/setup" 26 | "os" 27 | 28 | "github.com/mitchellh/go-homedir" 29 | "github.com/spf13/cobra" 30 | 31 | "github.com/pygmystack/pygmy/external/docker/commands" 32 | ) 33 | 34 | // restartCmd represents the restart command 35 | var restartCmd = &cobra.Command{ 36 | Use: "restart", 37 | Example: "pygmy restart", 38 | Short: "Restart all pygmy containers.", 39 | Long: `This command will trigger the Down and Up commands`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | 42 | Key, _ := cmd.Flags().GetString("key") 43 | NoKey, _ := cmd.Flags().GetBool("no-addkey") 44 | 45 | if NoKey { 46 | c.Keys = []setup.Key{} 47 | } else { 48 | FoundKey := false 49 | for _, v := range c.Keys { 50 | if v.Path == Key { 51 | FoundKey = true 52 | } 53 | } 54 | 55 | if !FoundKey { 56 | thisKey := setup.Key{ 57 | Path: Key, 58 | } 59 | c.Keys = append(c.Keys, thisKey) 60 | } 61 | } 62 | 63 | commands.Restart(c) 64 | 65 | }, 66 | } 67 | 68 | func init() { 69 | 70 | homedir, _ := homedir.Dir() 71 | keypath := fmt.Sprintf("%v%v.ssh%vid_rsa", homedir, string(os.PathSeparator), string(os.PathSeparator)) 72 | 73 | rootCmd.AddCommand(restartCmd) 74 | restartCmd.Flags().StringP("key", "", keypath, "Path of SSH key to add") 75 | restartCmd.Flags().BoolP("no-addkey", "", false, "Skip adding the SSH key") 76 | restartCmd.Flags().BoolP("no-resolver", "", false, "Skip adding or removing the Resolver") 77 | } 78 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/pygmystack/pygmy/external/docker/setup" 26 | "os" 27 | "runtime" 28 | "strings" 29 | 30 | "github.com/mitchellh/go-homedir" 31 | "github.com/spf13/cobra" 32 | "github.com/spf13/viper" 33 | ) 34 | 35 | var ( 36 | cfgFile string 37 | c setup.Config 38 | validArgs = []string{"addkey", "clean", "down", "export", "pull", "restart", "status", "up", "update", "version"} 39 | ) 40 | 41 | // rootCmd represents the base command when called without any subcommands 42 | var rootCmd = &cobra.Command{ 43 | Use: "pygmy", 44 | ValidArgs: validArgs, 45 | Short: "amazeeio's local development tool", 46 | Long: `amazeeio's local development tool, 47 | 48 | Runs DNSMasq, HAProxy, MailHog and an SSH Agent in local containers for local development.`, 49 | // Uncomment the following line if your bare application 50 | // has an action associated with it: 51 | // Run: func(cmd *cobra.Command, args []string) { }, 52 | } 53 | 54 | // Execute adds all child commands to the root command and sets flags appropriately. 55 | // This is called by main.main(). It only needs to happen once to the rootCmd. 56 | func Execute() { 57 | if err := rootCmd.Execute(); err != nil { 58 | fmt.Println(err) 59 | os.Exit(1) 60 | } 61 | } 62 | 63 | func init() { 64 | cobra.OnInitialize(initConfig) 65 | 66 | // Here you will define your flags and configuration settings. 67 | // Cobra supports persistent flags, which, if defined here, 68 | // will be global for your application. 69 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", findConfig(), "") 70 | 71 | // Cobra also supports local flags, which will only run 72 | // when this action is called directly. 73 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 74 | } 75 | 76 | // findConfig will find the first available configuration and return a 77 | // sensible default any of the expected paths are not found. The default 78 | // is assigned to the default flag. If the result which is returned does 79 | // not exist, it will not be loaded into memory and it will not be reported. 80 | func findConfig() string { 81 | 82 | // Find home directory. 83 | home, err := homedir.Dir() 84 | if err != nil { 85 | fmt.Println(err) 86 | os.Exit(1) 87 | } 88 | 89 | // Find a config for non-windows. 90 | if runtime.GOOS != "windows" { 91 | 92 | // Define a list of files we need to search for. 93 | // The file needs to have an extension supported 94 | // by Viper and to be included in the strings 95 | // declared below. 96 | searchFor := []string{ 97 | home + "/.config/pygmy/config.yaml", 98 | home + "/.config/pygmy/config.yml", 99 | home + "/.config/pygmy/pygmy.yaml", 100 | home + "/.config/pygmy/pygmy.yml", 101 | home + "/.pygmy.yaml", 102 | home + "/.pygmy.yml", 103 | "/etc/pygmy/config.yaml", 104 | "/etc/pygmy/config.yml", 105 | "/etc/pygmy/pygmy.yaml", 106 | "/etc/pygmy/pygmy.yml", 107 | } 108 | 109 | // Look for each of the files listed above. 110 | for n := range searchFor { 111 | if _, err := os.Stat(searchFor[n]); err == nil { 112 | if !os.IsNotExist(err) { 113 | return searchFor[n] 114 | } 115 | } 116 | } 117 | } 118 | 119 | // Provide a default. 120 | if runtime.GOOS == "linux" { 121 | return strings.Join([]string{"etc", "pygmy", "config.yml"}, string(os.PathSeparator)) 122 | } 123 | return strings.Join([]string{home, ".pygmy.yml"}, string(os.PathSeparator)) 124 | } 125 | 126 | // initConfig reads in config file and ENV variables if set. 127 | func initConfig() { 128 | 129 | if cfgFile == "" { 130 | viper.SetConfigFile(findConfig()) 131 | } else { 132 | viper.SetConfigFile(cfgFile) 133 | } 134 | 135 | // If a config file is found, read it in. 136 | if err := viper.ReadInConfig(); err == nil { 137 | if os.Args[1] != "completion" && !jsonOutput { 138 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /cmd/status.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/spf13/cobra" 27 | 28 | "github.com/pygmystack/pygmy/external/docker/commands" 29 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 30 | ) 31 | 32 | var jsonOutput bool 33 | 34 | // statusCmd represents the status command 35 | var statusCmd = &cobra.Command{ 36 | Use: "status", 37 | Example: "pygmy status", 38 | Short: "Report status of the pygmy services", 39 | Long: `Loop through all of pygmy's services and identify the present state. 40 | This includes the docker services, the resolver and SSH key status`, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | 43 | if jsonOutput { 44 | c.JSONFormat = true 45 | } 46 | 47 | cli, ctx, err := internals.NewClient() 48 | if err != nil { 49 | fmt.Println(err) 50 | } 51 | 52 | commands.Status(ctx, cli, c) 53 | 54 | }, 55 | } 56 | 57 | func init() { 58 | 59 | rootCmd.AddCommand(statusCmd) 60 | statusCmd.Flags().BoolVarP(&jsonOutput, "json", "", false, "Output status in JSON format") 61 | 62 | } 63 | -------------------------------------------------------------------------------- /cmd/stop.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/spf13/cobra" 26 | 27 | "github.com/pygmystack/pygmy/external/docker/commands" 28 | ) 29 | 30 | // stopCmd represents the stop command 31 | var stopCmd = &cobra.Command{ 32 | Use: "stop", 33 | Example: "pygmy stop", 34 | Short: "Stop and do not remove all pygmy services", 35 | Long: `Check if any pygmy containers are running and removes 36 | then if they are, it will not attempt to remove any 37 | services which are not running.`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | 40 | err := commands.Stop(c) 41 | if err != nil { 42 | fmt.Println(err) 43 | } 44 | 45 | }, 46 | } 47 | 48 | func init() { 49 | 50 | rootCmd.AddCommand(stopCmd) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cmd/up.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | 27 | "github.com/mitchellh/go-homedir" 28 | "github.com/pygmystack/pygmy/external/docker/commands" 29 | "github.com/pygmystack/pygmy/external/docker/setup" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | // upCmd represents the up command 34 | var upCmd = &cobra.Command{ 35 | Use: "up", 36 | Aliases: []string{"start"}, 37 | Example: "pygmy up", 38 | Short: "Bring up pygmy services (dnsmasq, haproxy, mailhog, resolv, ssh-agent)", 39 | Long: `Launch Pygmy - a set of containers and a resolver with very specific 40 | configurations designed for use with Amazee.io local development. 41 | 42 | It includes dnsmasq, haproxy, mailhog, resolv and ssh-agent.`, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | 45 | Key, _ := cmd.Flags().GetString("key") 46 | NoKey, _ := cmd.Flags().GetBool("no-addkey") 47 | noResolv, _ := cmd.Flags().GetBool("no-resolver") 48 | 49 | if noResolv { 50 | c.ResolversDisabled = true 51 | } 52 | 53 | if NoKey { 54 | c.Keys = []setup.Key{} 55 | } else { 56 | 57 | keyExistsInConfig := false 58 | for _, key := range c.Keys { 59 | if key.Path == Key { 60 | keyExistsInConfig = true 61 | } 62 | } 63 | 64 | if !keyExistsInConfig { 65 | thisKey := setup.Key{ 66 | Path: Key, 67 | } 68 | c.Keys = append(c.Keys, thisKey) 69 | } 70 | } 71 | 72 | err := commands.Up(c) 73 | if err != nil { 74 | fmt.Println(err) 75 | } 76 | 77 | }, 78 | } 79 | 80 | func init() { 81 | 82 | homedir, _ := homedir.Dir() 83 | keypath := fmt.Sprintf("%v%v.ssh%vid_rsa", homedir, string(os.PathSeparator), string(os.PathSeparator)) 84 | 85 | rootCmd.AddCommand(upCmd) 86 | upCmd.Flags().StringP("key", "", keypath, "Path of SSH key to add") 87 | upCmd.Flags().BoolP("no-addkey", "", false, "Skip adding the SSH key") 88 | upCmd.Flags().BoolP("no-resolver", "", false, "Skip adding or removing the Resolver") 89 | } 90 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "github.com/spf13/cobra" 26 | 27 | "github.com/pygmystack/pygmy/external/docker/commands" 28 | ) 29 | 30 | // updateCmd represents the update command 31 | var updateCmd = &cobra.Command{ 32 | Use: "update", 33 | Aliases: []string{"pull"}, 34 | Example: "pygmy update", 35 | Short: "Pulls Docker Images and recreates the Containers", 36 | Long: `Pull all images Pygmy uses, as well as any images containing 37 | the string 'uselagoon', which encompasses all lagoon images.`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | 40 | err := commands.Update(c) 41 | if err != nil { 42 | fmt.Println(err) 43 | } 44 | 45 | }, 46 | } 47 | 48 | func init() { 49 | 50 | rootCmd.AddCommand(updateCmd) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Karl Hepworth 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to 5 | // deal in the Software without restriction, including without limitation the 6 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | // sell copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | // IN THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "github.com/spf13/cobra" 25 | 26 | "github.com/pygmystack/pygmy/external/docker/commands" 27 | ) 28 | 29 | // versionCmd represents the version command 30 | var versionCmd = &cobra.Command{ 31 | Use: "version", 32 | Example: "pygmy version", 33 | Short: "# Check current installed version of pygmy", 34 | Long: ``, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | 37 | commands.Version(c) 38 | 39 | }, 40 | } 41 | 42 | func init() { 43 | 44 | rootCmd.AddCommand(versionCmd) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /docs/connect_to_mysql_from_external.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | This section is outdated and needs an update 3 | 4 | # Connect to MySQL in Docker Container 5 | 6 | If you like to connect to the MySQL Database inside the Docker container with an external Tool like [Sequel Pro](http://www.sequelpro.com/), [MySQL Workbench](http://www.mysql.com/products/workbench/), [HeidiSQL](http://www.heidisql.com/), [DBeaver](http://dbeaver.jkiss.org/), just plain old `mysql` cli or anything else. 7 | 8 | ### Get published mysql port from container 9 | 10 | Docker assigns a randomly published port for MySQL during each container start. This is done to prevent port collisions. 11 | 12 | To get the published port via `docker`: 13 | 14 | $ docker port changeme.net.docker.amazee.io 15 | 3306/tcp -> 0.0.0.0:32797 16 | 17 | Or via `docker-compose` inside a Drupal repository 18 | 19 | $ docker-compose port drupal 3306 20 | 0.0.0.0:32797 21 | 22 | ### `linux` Get ip from container 23 | 24 | If you are on Linux and run docker native, you also need to get the IP of the container 25 | 26 | $ docker inspect --format '{{ .NetworkSettings.IPAddress }}' changeme.net.docker.amazee.io 27 | 172.17.0.4 28 | 29 | ### Connect to MySQL 30 | 31 | | | Linux | OS X | 32 | |----------|-------------------------------|-------------------------------| 33 | | IP/Host | IP from container | `docker.amazee.io` | 34 | | Port | published port from container | published port from container | 35 | | Username | `drupal` | `drupal` | 36 | | Password | `drupal` | `drupal` | 37 | | Database | `drupal` | `drupal` | 38 | 39 | #### Example Sequel PRO 40 | 41 | ![Screenshot SequelPro](images/Sequel_Pro.png) 42 | -------------------------------------------------------------------------------- /docs/customisation/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples of Pygmy customisation 3 | summary: Examples of the customisation options available in Pygmy 4 | authors: 5 | - Karl Hepworth 6 | date: 2020-01-28 7 | --- 8 | # Introduction 9 | 10 | The following are examples of how somebody can utilise pygmy to customise their environment using a `~/.pygmy.yml` file. This file will have a schema which can be imported and the services match the Docker API. 11 | 12 | **Standard schema for `~/.pygmy.yml`** 13 | ```yaml 14 | # Defaults is a boolean which indicates all default settings should be inherited. 15 | defaults: true 16 | 17 | # Resolvers is the Resolv configuration, you can disable this by setting it to []. 18 | resolvers: 19 | - Data: "Contents of the resolvr file/section" 20 | File: "filename.conf" 21 | Folder: "/folderpath" 22 | Name: "Human-readable name" 23 | 24 | # Services is a hashmap of 25 | services: 26 | 27 | # The hashmap entry denotes the service name - such as "amazeeio-dnsmasq". 28 | mycontainer: 29 | 30 | # Config is derrived from the Docker API, intended for container configuration. 31 | # See https://godoc.org/github.com/docker/docker/api/types/container#Config for the full spec. 32 | Config: 33 | 34 | # This field is MANDATORY as the value will by default be empty. 35 | Image: imagename 36 | 37 | # Labels is a key/value pair of labels which will evaluate to string 38 | # equivelants. For example, Booleans can evaluate to 0/1 or true/false 39 | # string values depending on the Docker runtime you're using. 40 | Labels: 41 | 42 | # To enable Pygmy to the configuration, you will need this label. 43 | # This field is MANDATORY as the value will by default be false. 44 | pygmy.enable: true 45 | 46 | # You need to give this container a name 47 | # This field is MANDATORY as the value will by default be empty. 48 | pygmy.name: mycontainer 49 | 50 | # If you are customising an existing service, you can optionally 51 | # inherit the defaults if the global defaults are disabled. 52 | # Setting this value on a non-standard service will do nothing. 53 | pygmy.defaults: true 54 | 55 | # To display the output when the container starts: 56 | pygmy.output: true 57 | 58 | # To hide the container from the status messages: 59 | pygmy.discrete: true 60 | 61 | # To test an endpoint: 62 | pygmy.url: http://mycontainer.docker.amazee.io 63 | 64 | # To identify the purpose of a container - this is rather specialised so please ignore. 65 | pygmy.purpose: sshagent 66 | 67 | # To set a weight between 10 and 99 to control the order containers are started: 68 | pygmy.weight: 50 69 | 70 | # HostConfig is derived from the Docker API, intended for host configuration. 71 | # See https://godoc.org/github.com/docker/docker/api/types/container#HostConfig for the full spec. 72 | HostConfig: [] 73 | 74 | # NetworkConfig is derived from the Docker API, intended for network configuration. 75 | # See https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig for the full spec. 76 | NetworkConfig: 77 | 78 | # Hashmap value ideally should be the network name - but could be anything. 79 | # Results may vary, so try what works. 80 | amazeeio-network: 81 | 82 | # Every network needs a name. 83 | Name: amazeeio-network 84 | 85 | # An array of Containers. 86 | Containers: 87 | 88 | # Container name will tell Pygmy to integrate the container of the specified name should be connected to the docker network. 89 | Name: amazeeio-haproxy 90 | 91 | Labels: 92 | 93 | # Mandatory for network creation/usage via Pygmy. 94 | pygmy.network: true 95 | 96 | # networks is a hashmap of the API for a NetworkResource. 97 | # See https://godoc.org/github.com/docker/docker/api/types#NetworkResource for the full spec. 98 | networks: [] 99 | 100 | # volumes is a hashmap of the API for Volumes 101 | # See https://godoc.org/github.com/docker/docker/api/types#Volume for the full spec. 102 | volumes: [] 103 | 104 | # keys is all of the SSH key paths which you're utilising. 105 | keys: 106 | - path: /home/user1/.ssh/id_rsa 107 | - path: /home/user2/.ssh/id_rsa 108 | ``` 109 | 110 | ## Applied examples 111 | 112 | A suite of examples with a specific purpose are on the way. -------------------------------------------------------------------------------- /docs/drupal_site_containers.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | This section is outdated and needs an update 3 | 4 | 5 | # Drupal Docker Containers 6 | 7 | 8 | During [Part I](./local_docker_development.md#part-i-shared-docker-containers) we just started the shared Docker containers. For each Drupal Site we need an own Docker Container: 9 | 10 | ## Prerequisites 11 | * [Docker Compose](https://docs.docker.com/compose/install/) 12 | * On OS X just run `brew install docker-compose` 13 | * On Linux use your favorite package manager or 14 | 15 | ## Find the right `docker-compose.yml` 16 | 17 | 1. Visit https://github.com/amazeeio/docker or clone https://github.com/amazeeio/docker.git into a folder on your computer 18 | 2. Copy the desired example file into your Drupal directory (see descriptions below). Use `example-docker-compose-drupal.yml` if unsure. 19 | 3. Rename the file to `docker-compose.yml` 20 | 4. Edit the file according to your needs, change at least the host name. _BTW: It's perfectly fine to commit this file into your git repository, so others that are also using amazee.io docker can use it as well._ 21 | 5. Run in the same directory as the `docker-compose.yml`: 22 | 23 | docker-compose up -d 24 | 6. If you are on Windows add the URL to the Hosts file (see [windows documentation](local_docker_development/windows.md) for that). 25 | 7. Open your browser with the entered URL in the `docker-compose.yml`, happy Drupaling! 26 | 27 | ## Connect to the container 28 | 29 | To run commands like `git` or other things within the container, you need to connect to the container. 30 | 31 | There are two ways for that: 32 | 33 | ### Connect via `docker-compose` (easier) 34 | 35 | This is the easier way, you need to be in the same folder where also the `docker-compose.yml` for that to work: 36 | 37 | docker-compose exec --user drupal drupal bash 38 | 39 | ### Connect via `docker` 40 | 41 | If you want to connect to a container wherever you are right now with your bash: 42 | 43 | docker exec -itu drupal example.com.docker.amazee.io bash 44 | 45 | *Replace `example.com.docker.amazee.io` with the docker container you want to connect to* 46 | 47 | ### Drush from your host machine 48 | 49 | To use Drush, you can either connect to the container as above, or add a bash function that will connect for you to run your Drush command. To add the function, add this to your .bashrc file: 50 | 51 | Bash: 52 | ``` 53 | function ddrush() { 54 | args="" 55 | while [ "$1" != "" ]; do 56 | args="${args} '$1'" && shift 57 | done; 58 | 59 | docker-compose exec --user drupal drupal bash -c "source ~/.bash_envvars && cd \"$AMAZEEIO_WEBROOT\" && PATH=`pwd`/../vendor/bin:\$PATH && drush ${args}" 60 | } 61 | ``` 62 | 63 | Fish Shell - ([fishshell.com](https://fishshell.com/)): 64 | ``` 65 | function ddrush --description 'Drush fish (friendly interactive shell) function that detects Amazee.io Docker container. ' 66 | if test -f (git rev-parse --show-toplevel)/.amazeeio.yml 67 | echo "Using Amazee.io Docker Container Drush" 68 | command docker-compose exec --user drupal drupal bash -c "source ~/.bash_envvars && cd \"$AMAZEEIO_WEBROOT\" && PATH=`pwd`/../vendor/bin:\$PATH && drush $argv" 69 | else 70 | command drush $argv 71 | end 72 | end 73 | 74 | funcsave drush 75 | ``` 76 | 77 | When you next start a bash session, you'll be able to use `ddrush` just like your normal `drush` command. 78 | 79 | ## Update Images 80 | 81 | We constantly make improvements, updates and some other nice things to our container images. Visit [changelog.amazee.io](https://changelog.amazee.io) to see if there is something new. If you need to update the Docker Images to the newest version from the Docker Hub run in the same folder as the `docker-compose.yml`: 82 | 83 | docker-compose pull 84 | docker-compose up -d 85 | 86 | ### Slow Updates? 87 | 88 | When pulling a new docker image, the download can get stuck. There is a nice workaround for that: pull a second time :) 89 | 90 | Just open another terminal window at the exact same directory than you run the first `docker-compose pull` and just run that command again. The download will be unstuck and continue again. If the download is stuck again, cancel the second command with CTRL+c, and run it again (no worries, the first one will continue to run). Repeat that until the download is completely done. 91 | 92 | 93 | ## `docker-compose.yml` example files 94 | 95 | | Example File | PHP | Services | Description | 96 | |----------------------------------------------------------------------------------------------------------------------------------|--------|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------| 97 | | [`example-docker-compose-drupal.yml`](https://github.com/amazeeio/docker/blob/master/example-docker-compose-drupal.yml) | 5.6/7.0/7.1| nginx, varnish, mariadb |Drupal container without provisions for solr. See comments in file for choosing PHP version and customizing for Composer sites | 98 | | [`example-docker-compose-drupal-solr.yml`](https://github.com/amazeeio/docker/blob/master/example-docker-compose-drupal-solr.yml)| 5.6/7.0/7.1| nginx, varnish, mariadb, solr |Drupal container with provisions for solr. See comments in file for choosing PHP version and customizing for Composer sites | 99 | -------------------------------------------------------------------------------- /docs/images/Sequel_Pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pygmystack/pygmy/2cb96865d6205cf331171954e73cc4310ee2f6e4/docs/images/Sequel_Pro.png -------------------------------------------------------------------------------- /docs/images/Statistics_Report_for_HAProxy_on_c06d7fc60984.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pygmystack/pygmy/2cb96865d6205cf331171954e73cc4310ee2f6e4/docs/images/Statistics_Report_for_HAProxy_on_c06d7fc60984.jpg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Pygmy 2 | 3 | `pygmy` is the single tool needed to get the local [amazee.io](https://amazee.io) Docker Drupal Development Environment running on your Linux based system. It built to work with [Docker for Mac](https://docs.docker.com/docker-for-mac/)! (quite a lot for such a [small whale](https://en.wikipedia.org/wiki/Pygmy_sperm_whale) 🐳) 4 | 5 | **What `pygmy` will handle for you:** 6 | 7 | * Starting the necessary Docker Containers for the amazee.io Drupal Docker Development 8 | * If on Linux: Adds `nameserver 127.0.0.1` to your `/etc/resolv.conf` file, so that your local Linux can resolve `*.docker.amazee.io` via the dnsmasq container 9 | * If on Mac with Docker for Mac: Creates the file `/etc/resolver/docker.amazee.io` which tells OS X to forward DNS requests for `*.docker.amazee.io` to the dnsmasq container 10 | * Tries to add the ssh key in `~/.ssh/id_rsa` to the ssh-agent container (no worries if that is the wrong key, you can add more any time) 11 | * Starts a local mail Mail Transfer Agent (MTA) in order to test and view mails 12 | 13 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | #Installation of Pygmy 2 | 3 | ## Prerequisites 4 | Make sure you have the following dependencies installed: 5 | 6 | * Docker, see [the official guides](https://docs.docker.com/engine/installation/) on how to install docker on your system. 7 | * Go (optional), see [the official guides](https://golang.org/doc/install) 8 | 9 | ## Installation 10 | 11 | ### Installing from a precompiled binary 12 | 13 | Releases on GitHub accompany binaries [available for download](https://github.com/pygmystack/pygmy/releases). 14 | 15 | To install it, put the binary into your system's `$PATH` environment variable and make it executable. 16 | 17 | The following is an example of how you would do this, note the URL and location may change depending on your needs. 18 | ```console 19 | $ wget https://github.com/pygmystack/pygmy/releases/download/v0.8.0/pygmy-darwin 20 | $ mv ./pygmy-darwin /usr/local/bin/pygmy 21 | $ chmod u+x /usr/local/bin/pygmy 22 | ``` 23 | 24 | ### Build from source 25 | 26 | Pygmy comes with a Make file, which you can simply run `make build && make clean` to build binaries for Linux (amd64), Windows (x86) & MacOS (Darwin). 27 | 28 | From here you can follow the guidance to install the relevant executable in the `builds/` folder usign the instructions above. 29 | 30 | ### Installing from source 31 | 32 | The installation of `pygmy` is fairly simple and can be accomplished via the go toolchain 33 | 34 | ```console 35 | $ go get github.com/pygmystack/pygmy 36 | ``` -------------------------------------------------------------------------------- /docs/local_docker_development.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | This section is outdated and needs an update 3 | 4 | # Local Drupal Docker Development 5 | 6 | amazee.io supports development workflows which involve local development sites. We provide a Drupal Docker development environment that runs on your local computer. 7 | It uses the exact same configuration for **all** services like on the amazee.io servers. This means: 8 | 9 | * If the site runs locally, it also runs on production 10 | * You can use the exact same `settings.php` file for local and production 11 | 12 | **And the best:** You don't need to have any amazee.io account or site running in order to use the local development environment! Just install it, and experience all the benefits of amazee.io for free. 13 | 14 | The Docker based Drupal Development environment consists of two parts: 15 | 16 | ### Part I: Shared Docker Containers 17 | 18 | The shared docker containers for HAProxy and the SSH Agent, these are used by all other containers in order to properly work. They are started with `pygmy` for Linux & OS X. 19 | 20 | ### [Part II: Drupal Docker Containers](./drupal_site_containers.md) 21 | 22 | The Docker Containers which will run Drupal. These are made to be copied into a Drupal root directory and to be started from there with `docker-compose`.[ Read how they are used](./drupal_site_containers.md) 23 | 24 | ## What it includes 25 | 26 | The amazee.io Local Docker Drupal Development environment equips you with all the tools you need to develop your Drupal site locally: 27 | 28 | * **Webserver:** Nginx 29 | * **Frontend Caching:** Varnish 30 | * **FastCGI Process Manager:** PHP-FPM 31 | * **Server-side Scripting Language:** PHP 32 | * **Database:** MariaDB 33 | * **Search:** Apache Solr 34 | * **Dependency Manager for PHP:** Composer 35 | * NodeJS / NPM 36 | 37 | For more information about software components used in the amazee.io Stack head over to the [Components](../architecture/components.md) overview page. 38 | 39 | ## How this works 40 | 41 | Docker is super awesome and the perfect tool for local development. There are some hurdles though \(no worries, we have a solution for all of them\): 42 | 43 | #### Exposed ports 44 | 45 | If multiple Docker containers are exposing the same port it assigned a random port to the exposed port. In our case, this would mean, that each Drupal Container which would like to listen on Port 80 would get a random port like 34564 assigned. As they are random assigned it would be a lot of hassle of figuring out which port that the Drupal is found, additionally, Drupal doesn't like to run on another Port then 80 or 443 so much. 46 | 47 | #### SSH Keys 48 | 49 | It is possible to add mount ssh private keys into Docker containers, but this is again cumbersome, especially when you have a passphrase protected key \(as you should!\). You would need to enter the passphrase for each container that you start. Not a lot of fun. 50 | 51 | ### The Solution 52 | 53 | amazee.io implemented a Drupal Docker Development environment which handles all these issues nicely for you. It allows you to: 54 | 55 | * Access all sites via the Port 80 or 443 with just different URLs like site1.docker.amazee.io and site2.docker.amazee.io 56 | * Add your SSH Key once to the system and can forget about it, no need to add it to each container 57 | 58 | The environment starts 4 containers: 59 | 60 | * [andyshinn/dnsmasq](https://hub.docker.com/r/andyshinn/dnsmasq/) Docker container which will listen on port 53 and resolve all DNS requests from `*.docker.amazee.io` to `127.0.0.1` \(so basically a better way then filling your `/etc/hosts` file by hand\) 61 | * [amazeeio/haproxy](https://hub.docker.com/r/amazeeio/haproxy/) Docker container which will listen on port 80 and 443. It additionally listens to the Docker socket, realize when you start a new Drupal Container and adapt fully automatically it's haproxy configuration \(thanks to the awesome tool [docker-gen](https://github.com/jwilder/docker-gen)\). It forwards HTTP and HTTPs requests to the correct Drupal Container. With that we can access all Drupal Containers via a single Port. 62 | * [amazeeio/ssh-agent](https://hub.docker.com/r/amazeeio/ssh-agent/) Docker container which will keeps an ssh-agent at hand for the other Drupal Containers. With that the Drupal Containers do not need to handle ssh-agenting themselves 63 | * [mailhog/mailhog](https://hub.docker.com/r/mailhog/mailhog/) Docker container which will keeps emails from being sent but allows for you to read and debug message contents. 64 | 65 | #### Schema for Linux \(native Docker\) 66 | 67 | ``` 68 | +--------------------------------------------------------------------+ 69 | |Docker | 70 | | | 71 | | HAProxy knows which | 72 | | *.docker.amazee.io is | 73 | | handled by which container +---------------------+ | 74 | | | | | 75 | | +-------+ Drupal Container 1 <--+ | 76 | | | | | | | 77 | +--------------------+ | +------------------+ | +---------------------+ | | 78 | | | | | | | | | 79 | | | | | HAProxy +-----+ | | 80 | | +----------------------------> | | +---------------------+ | | 81 | | | | | Published Ports | | | | | | 82 | | | any HTTP/HTTPS | | 80/443 | +-------+ Drupal Container 2 <--+ | 83 | | | request | +------------------+ | | | | 84 | | Browser | | +---------------------+ | | 85 | | | | | | 86 | | | | +------------------+ | | 87 | | +----------------------------> | | | 88 | | <----------------------------+ dns masq | | | 89 | | | | | | | | 90 | | | Resolves | | | | | 91 | | | *.docker.amaze.io | | | | | 92 | +--------------------+ to IP of Haproxy | +------------------+ | | 93 | | | | 94 | | | | 95 | | | | 96 | | | | 97 | | | | 98 | +--------------------+ | +------------------+ | | 99 | | | | | | | | 100 | | pygmy +----------------------------> ssh agent +--------------------------------------+ | 101 | | | | | | | 102 | +--------------------+ injects ssh-key | +------------------+ Exposes ssh-agent via | 103 | into agent | /tmp/amazeeio_ssh-agent/socket | 104 | | | 105 | +--------------------------------------------------------------------+ 106 | ``` 107 | 108 | -------------------------------------------------------------------------------- /docs/map_addtitional_ports.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | This section is outdated and needs an update 3 | 4 | # Map additional ports 5 | 6 | As it explained in the [Connect to MySQL](./connect_to_mysql_from_external.md) section, Docker maps MySQL's 3306 port to a random port on docker.amazee.io. This happens because port 3306 is set in the `docker-compose.yml` file: 7 | ``` 8 | ports: 9 | - "3306" 10 | ``` 11 | 12 | If you need to map other ports, simply add them to the `ports` section and restart the container. 13 | 14 | ### Example for Solr 15 | 16 | If you use one of amazee.io Drupal containers with Solr included, your Solr URL most likely looks like this: http://127.0.0.1:8149/solr/drupal/ 17 | 18 | In this case, to play with Solr queries: 19 | - add `"8149"` to the `ports` section of `docker-compose.yml` file 20 | - restart the container with `docker-compose stop && docker-compose up -d` 21 | - get the port number with `docker-compose port drupal 8149` 22 | - start playing at http://docker.amazee.io:<PORT_NUMBER>/solr/drupal/admin/ 23 | -------------------------------------------------------------------------------- /docs/readthedocs.yml: -------------------------------------------------------------------------------- 1 | requirements_file: requirements.txt 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1 2 | -------------------------------------------------------------------------------- /docs/ssh_agent.md: -------------------------------------------------------------------------------- 1 | # SSH Agent 2 | 3 | Per default your SSH Key at `~/.ssh/id_rsa` is added to the Docker containers from `pygmy` 4 | 5 | If you need another key, read the documentation of [`pygmy`](linux_pygmy.md) about this. 6 | 7 | ## How it works 8 | 1. `pygmy` starts `amazeeio/ssh-agent` container with a volume `/tmp/amazeeio_ssh-agent` 9 | 2. `pygmy` adds a default SSH key from the host into this volume 10 | 3. `docker-compose.yml` should have volume inclusion specified for CLI container: 11 | ``` 12 | volumes_from: 13 | - container:amazeeio-ssh-agent 14 | ``` 15 | 4. When CLI container starts, the volume is mounted and an entrypoint script adds SHH key into agent. 16 | @see https://github.com/amazeeio/lagoon/blob/master/images/php/cli/10-ssh-agent.sh 17 | 18 | Running `ssh-add -L` within CLI container should show that the SSH key is correctly loaded. 19 | 20 | ## Troubleshooting 21 | ### SSH Key issues 22 | 23 | As everything on amazee.io works with key authentication sometimes you might run into issues where the drush aliases aren't displayed or you can't connect to the servers. 24 | 25 | Could not load API JWT Token, error was: 'lagoon@ssh.lagoon.amazeeio.cloud: Permission denied (publickey).' 26 | 27 | Or for legacy systems: 28 | 29 | drupal@example.amazee.io:~/public_html/docroot (staging)$ drush @master ssh 30 | Permission denied (publickey). 31 | 32 | 1. Check if you see the SSH Key inside your container with `ssh-add -L`
33 | If you get `Could not open a connection to your authentication agent.` or `The agent has no identities.` head straight to **step 3.** 34 | 2. Check if you see your SSH Key in `pygmy status` 35 | 3. If you don't see the key in `pymgy status` run `pygmy addkey`. You should see `Successfully added ssh key` if the key addition was successful. 36 | 4. After that you need to recreate the containers `docker-compose up -d --force` 37 | 5. When the containers are recreated you should be able to see your ssh key with `ssh-add -L` 38 | 6. If you still get the `Permission denied (publickey)` error get in touch with our engineers to check if the key is configured correctly on the hosting side. 39 | -------------------------------------------------------------------------------- /docs/update.md: -------------------------------------------------------------------------------- 1 | # Update pygmy 2 | 3 | As `pygmy` is an active project, you should also take care of updating pygmy. 4 | 5 | Use the [same instructions](./installation.md) to update Pygmy as to install it. 6 | 7 | ## I see errors or unexpected behaviour after the upgrade 8 | 9 | If you see anything unexpected after upgrading, the recommended advice is to clean up the environment _and_ remove the docker network. 10 | 11 | Any applications which use the network `amazeeio-network` such as a docker-compose Drupal project - should not be running. You can alternatively run `docker network rm amazeeio-network --force`. 12 | 13 | ```console 14 | $ pygmy clean 15 | $ docker network rm amazeeio-network 16 | ``` 17 | 18 | ## Update Docker Containers with `pygmy` 19 | 20 | `pygmy` can update shared docker containers for you: 21 | 22 | pygmy update && pygmy restart 23 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | 2 | ## Start 3 | To start `pygmy` run following command 4 | 5 | pygmy up 6 | 7 | `pygmy` will now start all the required Docker containers and add the ssh key. 8 | 9 | If you are on Ubuntu you might need to run pygmy with `pygmy up --no-resolver` 10 | 11 | **All done?** Head over to [Drupal Docker Containers](./drupal_site_containers.md) to learn how to work with docker containers. 12 | 13 | # Command line usage 14 | 15 | ``` 16 | Amazeeio's local development tool, 17 | 18 | Runs DNSMasq, HAProxy, MailHog and an SSH Agent in local containers for local development. 19 | 20 | Usage: 21 | pygmy [command] 22 | 23 | Available Commands: 24 | addkey Add/re-add an SSH key to the agent 25 | clean Stop and remove all pygmy services regardless of state 26 | down Stop and remove all pygmy services 27 | export Export validated configuration to a given path 28 | help Help about any command 29 | restart Restart all pygmy containers. 30 | status Report status of the pygmy services 31 | up Bring up pygmy services (dnsmasq, haproxy, mailhog, resolv, ssh-agent) 32 | update Pulls Docker Images and recreates the Containers 33 | version # Check current installed version of pygmy 34 | 35 | Flags: 36 | --config string config file (default is $HOME/.pygmy.yml) 37 | -h, --help help for pygmy 38 | -t, --toggle Help message for toggle 39 | 40 | Use "pygmy [command] --help" for more information about a command. 41 | ``` 42 | 43 | 44 | 45 | ## Adding ssh keys 46 | 47 | Call the `addkey` command with the **absolute** path to the key you would like to add. In case this they is passphrase protected, it will ask for your passphrase. 48 | 49 | pygmy addkey /Users/amazeeio/.ssh/my_other_key 50 | 51 | Enter passphrase for /Users/amazeeio/.ssh/my_other_key: 52 | Identity added: /Users/amazeeio/.ssh/my_other_key (/Users/amazeeio/.ssh/my_other_key) 53 | 54 | ## Checking the status 55 | 56 | Run `pygmy status` and `pygmy` will tell you how it feels right now and which ssh-keys it currently has in it's stomach: 57 | 58 | pygmy status 59 | 60 | [*] amazeeio-ssh-agent: Running as container amazeeio-ssh-agent 61 | [*] mailhog.docker.amazee.io: Running as container mailhog.docker.amazee.io 62 | [*] amazeeio-haproxy: Running as container amazeeio-haproxy 63 | [*] amazeeio-dnsmasq: Running as container amazeeio-dnsmasq 64 | [*] amazeeio-haproxy is connected to network amazeeio-network 65 | [*] Resolv MacOS Resolver is properly connected 66 | �ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDNxWpKZcU/D+t7ToRGPNEXbvojrFtxKH99ZuaOJ7cs9KurVJyiEHyBEUZAPt0j9SO5yzdVEM//rVoZIwZeypW9C7CYgTpRoA/k1BnE1xvtoQT+528GmjQG542NBFo2KdO+LWqx19kClvoN7haGDtYKbS6MWUYEwD0ey69cquFDKC+A5NKx3z065gn9UZqLIeXjHCJ+v5PCSWXL3CFn57UlN824j1OFAECrjfNNfFEVmDJqa2Da6o9DhN+W1wyZJCklRPCiRlK5m3p9x1ClPKALUGQ0hvpjz36QSsXqS88MJPHsZvsv2PuW6xXNW8PSBCHcK6no5lYV/4hk8jcDQd2P6dpwvDiti+bTcfDH3jrVNqFati7ku37xIc3jWGn7CkCpMy008ai4kFMq2W2w6gOy0HncQ7z8AE8BdndxyEFYCLJviWOjW1SjSesPJpc9dxgmSmp/2qa6u0UZzFFHxJklIHepJAvcoHghs5Te2oMHwriRdpKqXiW+eJyudWCOzEeJljr73/Caft+CgZ7+kmmiy0hlqVAD6xkyBsuEF8+MdONfBHarpY8qZdLehavGd0DJW36nDnPvefDxoidJ0qYtjF8ElpNkeguAnsUFEwHkoc3Ur/NDcrkdGTKS8wb5AtkdwbDOCQTR00ABfAcYUFwOAvXodoQLrvm2ibp5l7/Y/Q== user@localhost 67 | - http://mailhog.docker.amazee.io (mailhog.docker.amazee.io) 68 | - http://docker.amazee.io/stats (amazeeio-haproxy) 69 | 70 | ## `pygmy down` vs `pygmy clean` 71 | 72 | `pygmy` behaves like Docker, it's a whale in the end! 73 | 74 | During regular development `pygmy stop` is perfectly fine, it will remove the Docker containers still alive. 75 | 76 | If you like to cleanup though, use `pygmy clean` to kill and remove all of the Docker containers, even if they're not alive. 77 | 78 | ## Access HAProxy statistic page and logs 79 | 80 | HAProxy service has statistics web page already enabled. To access the page, just point the browser to [http://docker.amazee.io/stats](http://docker.amazee.io/stats). 81 | 82 | To watch at haproxy container logs, use the `docker logs amazeeio-haproxy` command with standard `docker logs` options like `-f` to follow. 83 | -------------------------------------------------------------------------------- /examples/pygmy.basic.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | amazeeio-dnsmasq: 4 | Config: 5 | Labels: 6 | - pygmy.hocuspocus: 42 7 | - pygmy.abracadabra: true 8 | - pygmy.opensesame: correct 9 | 10 | amazeeio-haproxy: 11 | Config: 12 | Labels: 13 | - pygmy.hocuspocus: 42 14 | - pygmy.abracadabra: true 15 | - pygmy.opensesame: correct 16 | 17 | amazeeio-mailhog: 18 | Config: 19 | Labels: 20 | - pygmy.hocuspocus: 42 21 | - pygmy.abracadabra: true 22 | - pygmy.opensesame: correct 23 | 24 | amazeeio-ssh-agent: 25 | Config: 26 | Labels: 27 | - pygmy.hocuspocus: 42 28 | - pygmy.abracadabra: true 29 | - pygmy.opensesame: correct 30 | 31 | amazeeio-ssh-agent-add-key: 32 | Config: 33 | Labels: 34 | - pygmy.enable: false 35 | 36 | pygmy-cowsay: 37 | Config: 38 | Image: mbentley/cowsay 39 | Cmd: 40 | - holy 41 | - ship 42 | Labels: 43 | - pygmy.enable: true 44 | - pygmy.discrete: false 45 | - pygmy.name: pygmy-cowsay 46 | - pygmy.output: true 47 | - pygmy.weight: 99 48 | HostConfig: 49 | AutoRemove: true 50 | -------------------------------------------------------------------------------- /examples/pygmy.complex.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | services: 4 | 5 | amazeeio-haproxy: 6 | Config: 7 | Labels: 8 | - pygmy.enable: false 9 | 10 | amazeeio-dnsmasq: 11 | Config: 12 | Labels: 13 | - pygmy.enable: false 14 | 15 | amazeeio-mailhog: 16 | Config: 17 | Labels: 18 | - pygmy.enable: true 19 | - pygmy.name: amazeeio-mailhog 20 | - pygmy.network: amazeeio-network 21 | - traefik.enable: true 22 | - traefik.port: 80 23 | - traefik.http.routers.mailhog.rule: Host(`mailhog.docker.amazee.io`) 24 | 25 | unofficial-phpmyadmin: 26 | Config: 27 | Image: phpmyadmin/phpmyadmin 28 | Env: 29 | - "PMA_ARBITRARY=1" 30 | Labels: 31 | - pygmy.enable: true 32 | - pygmy.name: unofficial-phpmyadmin 33 | - pygmy.network: amazeeio-network 34 | - pygmy.url: http://phpmyadmin.docker.amazee.io 35 | - pygmy.weight: 20 36 | - traefik.enable: true 37 | - traefik.port: 80 38 | - traefik.http.routers.phpmyadmin.rule: Host(`phpmyadmin.docker.amazee.io`) 39 | HostConfig: 40 | PortBindings: 41 | 80/tcp: 42 | - HostPort: 8770 43 | 44 | unofficial-traefik-2: 45 | Config: 46 | Image: library/traefik:v2.1.3 47 | Cmd: 48 | - --api 49 | - --api.insecure=true 50 | - --providers.docker 51 | - --providers.docker.exposedbydefault=false 52 | - --providers.docker.defaultrule=Host(`{{ index .Labels "com.docker.compose.project" }}.docker.amazee.io`) 53 | - --entrypoints.web.address=:80 54 | - --entrypoints.websecure.address=:443 55 | ExposedPorts: 56 | 80/tcp: 57 | HostPort: 80 58 | 443/tcp: 59 | HostPort: 443 60 | 8080/tcp: 61 | HostPort: 3080 62 | Labels: 63 | - pygmy.enable: true 64 | - pygmy.name: unofficial-traefik-2 65 | - pygmy.network: amazeeio-network 66 | - pygmy.url: http://traefik.docker.amazee.io 67 | - traefik.docker.network: amazeeio-network 68 | - traefik.enable: true 69 | - traefik.port: 80 70 | - traefik.http.routers.traefik.rule: Host(`traefik.docker.amazee.io`) 71 | - traefik.http.routers.traefik.tls: true 72 | - traefik.http.routers.traefik.service: api@internal 73 | - traefik.providers.docker.defaultport: 8080 74 | HostConfig: 75 | Binds: 76 | - /var/run/docker.sock:/var/run/docker.sock 77 | PortBindings: 78 | 443/tcp: 79 | - HostPort: 443 80 | 80/tcp: 81 | - HostPort: 80 82 | 8080/tcp: 83 | - HostPort: 8080 84 | RestartPolicy: 85 | Name: unless-stopped 86 | MaximumRetryCount: 0 87 | NetworkConfig: 88 | Ports: 89 | 80/tcp: 90 | - HostPort: 80 91 | 8080/tcp: 92 | - HostPort: 8080 93 | 94 | networks: 95 | amazeeio-network: 96 | Name: amazeeio-network 97 | Containers: 98 | amazeeio-haproxy: {} 99 | Labels: 100 | - pygmy.network: amazeeio-network 101 | 102 | resolvers: [] 103 | 104 | -------------------------------------------------------------------------------- /examples/pygmy.noresolv.yml: -------------------------------------------------------------------------------- 1 | resolversDisabled: true 2 | -------------------------------------------------------------------------------- /examples/pygmy.overrides.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | amazeeio-haproxy: 4 | image: ghcr.io/pygmystack/haproxy:main 5 | amazeeio-ssh-agent: 6 | image: ghcr.io/pygmystack/ssh-agent:main 7 | amazeeio-dnsmasq: 8 | image: ghcr.io/pygmystack/dnsmasq:main 9 | amazeeio-mailhog: 10 | image: ghcr.io/pygmystack/mailhog:main 11 | -------------------------------------------------------------------------------- /external/docker/commands/addkey.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "strings" 8 | 9 | . "github.com/logrusorgru/aurora" 10 | 11 | "github.com/pygmystack/pygmy/external/docker/setup" 12 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 13 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 14 | "github.com/pygmystack/pygmy/internal/service/docker/ssh/agent" 15 | "github.com/pygmystack/pygmy/internal/utils/color" 16 | ) 17 | 18 | // SshKeyAdd will add a given key to the ssh agent. 19 | func SshKeyAdd(c setup.Config, key string) error { 20 | cli, ctx, err := internals.NewClient() 21 | if err != nil { 22 | return err 23 | } 24 | 25 | setup.Setup(ctx, cli, &c) 26 | 27 | if key != "" { 28 | if _, err := os.Stat(key); err != nil { 29 | fmt.Printf("%v\n", err) 30 | return err 31 | } 32 | } else { 33 | return nil 34 | } 35 | 36 | for _, Container := range c.Services { 37 | purpose, _ := Container.GetFieldString(ctx, cli, "purpose") 38 | if purpose == "addkeys" { 39 | 40 | // Validate SSH Key before adding. 41 | valid, err := agent.Validate(key) 42 | if valid { 43 | color.Print(Green(fmt.Sprintf("Validation success for SSH key %v\n", key))) 44 | } else { 45 | if err.Error() == "ssh: this private key is passphrase protected" { 46 | color.Print(Green(fmt.Sprintf("Validation success for protected SSH key %v\n", key))) 47 | } 48 | if err.Error() == "ssh: no key found" { 49 | return fmt.Errorf("[ ] Validation failure for SSH key %v\n", key) 50 | } 51 | } 52 | 53 | if runtime.GOOS == "windows" { 54 | Container.Config.Cmd = []string{"windows-key-add", "/key"} 55 | Container.HostConfig.Binds = append(Container.HostConfig.Binds, fmt.Sprintf("%v:/key", key)) 56 | } else { 57 | Container.Config.Cmd = []string{"ssh-add", key} 58 | Container.HostConfig.Binds = append(Container.HostConfig.Binds, fmt.Sprintf("%v:%v", key, key)) 59 | } 60 | 61 | if err := Container.Create(ctx, cli); err != nil { 62 | _ = Container.Remove(ctx, cli) 63 | return err 64 | } 65 | if err := Container.Start(ctx, cli); err != nil { 66 | _ = Container.Remove(ctx, cli) 67 | return err 68 | } 69 | 70 | interactive, _ := Container.GetFieldBool(ctx, cli, "interactive") 71 | name, _ := Container.GetFieldString(ctx, cli, "name") 72 | if !interactive { 73 | l, _ := containers.Logs(ctx, cli, name) 74 | handled := false 75 | // We need tighter control on the output of this container... 76 | for _, line := range strings.Split(string(l), "\n") { 77 | if strings.Contains(line, "Identity added:") { 78 | handled = true 79 | color.Print(Green(fmt.Sprintf("Successfully added SSH key %v to agent\n", key))) 80 | } 81 | if strings.Contains(line, "Enter passphrase for") { 82 | handled = true 83 | color.Print(Yellow("Warning: Passphrase protected SSH keys can only be added in interactive mode, the key will not be added.\n")) 84 | } 85 | } 86 | 87 | // Logs didn't contain known messages, log all in case of error. 88 | if !handled { 89 | color.Print(Red("Unknown error while adding SSH key:\n")) 90 | for _, line := range strings.Split(string(l), "\n") { 91 | fmt.Println(line) 92 | } 93 | } 94 | } 95 | 96 | _ = Container.Remove(ctx, cli) 97 | 98 | } 99 | 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /external/docker/commands/clean.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | . "github.com/logrusorgru/aurora" 8 | 9 | "github.com/pygmystack/pygmy/external/docker/setup" 10 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 11 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 12 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/networks" 13 | "github.com/pygmystack/pygmy/internal/utils/color" 14 | ) 15 | 16 | // Clean will forcibly kill and remove all of pygmy's containers in the daemon 17 | func Clean(c setup.Config) error { 18 | cli, ctx, err := internals.NewClient() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | setup.Setup(ctx, cli, &c) 24 | Containers, _ := containers.List(ctx, cli) 25 | NetworksToClean := []string{} 26 | 27 | for _, Container := range Containers { 28 | ContainerName := strings.Trim(Container.Names[0], "/") 29 | target := false 30 | if l := Container.Labels["pygmy.enable"]; l == "true" || l == "1" { 31 | target = true 32 | } 33 | if l := Container.Labels["pygmy"]; l == "pygmy" { 34 | target = true 35 | } 36 | if l := Container.Labels["pygmy.network"]; l != "" { 37 | NetworksToClean = append(NetworksToClean, l) 38 | } 39 | 40 | if target { 41 | err := containers.Kill(ctx, cli, Container.ID) 42 | if err == nil { 43 | color.Print(Green(fmt.Sprintf("Successfully killed %s\n", ContainerName))) 44 | } 45 | 46 | err = containers.Remove(ctx, cli, Container.ID) 47 | if err == nil { 48 | color.Print(Green(fmt.Sprintf("Successfully removed %s\n", ContainerName))) 49 | } 50 | } 51 | } 52 | 53 | for _, network := range c.Networks { 54 | NetworksToClean = append(NetworksToClean, network.Name) 55 | } 56 | 57 | for n := range setup.Unique(NetworksToClean) { 58 | if s, _ := networks.Status(ctx, cli, NetworksToClean[n]); s { 59 | e := networks.Remove(ctx, cli, NetworksToClean[n]) 60 | if e != nil { 61 | fmt.Println(e) 62 | } 63 | if s, _ := networks.Status(ctx, cli, NetworksToClean[n]); !s { 64 | color.Print(Green(fmt.Sprintf("Successfully removed network %s\n", NetworksToClean[n]))) 65 | } else { 66 | color.Print(Red(fmt.Sprintf("Failed to remove %s\n", NetworksToClean[n]))) 67 | } 68 | } 69 | } 70 | 71 | for _, resolver := range c.Resolvers { 72 | resolver.Clean() 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /external/docker/commands/down.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pygmystack/pygmy/external/docker/setup" 7 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 8 | ) 9 | 10 | // Down will bring pygmy down safely 11 | func Down(c setup.Config) error { 12 | cli, ctx, err := internals.NewClient() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | setup.Setup(ctx, cli, &c) 18 | for _, Service := range c.Services { 19 | enabled, _ := Service.GetFieldBool(ctx, cli, "enable") 20 | if enabled { 21 | e := Service.StopAndRemove(ctx, cli) 22 | if e != nil { 23 | name, _ := Service.GetFieldString(ctx, cli, "name") 24 | fmt.Printf("Failed to stop and remove %s\n", name) 25 | } 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /external/docker/commands/export.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/ghodss/yaml" 8 | 9 | "github.com/pygmystack/pygmy/external/docker/setup" 10 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 11 | ) 12 | 13 | // Export will export validated configuration to a given path, or it will 14 | // export by default to $HOME/.pygmy.yml 15 | func Export(c setup.Config, output string) error { 16 | cli, ctx, err := internals.NewClient() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | // Set up the configuration. 22 | setup.Setup(ctx, cli, &c) 23 | 24 | // Marshal to Yaml. 25 | x, err := yaml.Marshal(c) 26 | if err != nil { 27 | fmt.Println(err) 28 | return err 29 | } 30 | 31 | // Provide output for state. 32 | fmt.Println("Data has been marshalled to YAML") 33 | 34 | // Does the file exist? 35 | if _, e := os.Stat(output); !os.IsNotExist(e) { 36 | // Remove the existing file. 37 | if err := os.Remove(output); err != nil { 38 | fmt.Println(err) 39 | return err 40 | } 41 | 42 | // Provide output for state. 43 | fmt.Printf("Path %v has been removed\n", output) 44 | 45 | } 46 | 47 | if _, e := os.Stat(output); os.IsNotExist(e) { 48 | 49 | // Create the new file. 50 | file, err := os.Create(output) 51 | if err != nil { 52 | fmt.Println(err) 53 | return err 54 | } 55 | 56 | // Provide output for state. 57 | fmt.Printf("Path %v has been created\n", output) 58 | 59 | // Housekeeping. 60 | defer file.Close() 61 | 62 | _, err = file.WriteString(string(x)) 63 | if err != nil { 64 | fmt.Println(err) 65 | return err 66 | } 67 | 68 | // Provide output for state. 69 | fmt.Printf("Data has been written to file %v\n", output) 70 | 71 | err = file.Sync() 72 | if err != nil { 73 | fmt.Println(err) 74 | return err 75 | } 76 | 77 | // Provide output for state. 78 | fmt.Println("Operation completed successfully") 79 | } 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /external/docker/commands/restart.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pygmystack/pygmy/external/docker/setup" 6 | ) 7 | 8 | // Restart will stop and start Pygmy in its entirety. 9 | func Restart(c setup.Config) { 10 | err := Down(c) 11 | if err != nil { 12 | fmt.Println(err) 13 | } 14 | 15 | err = Up(c) 16 | if err != nil { 17 | fmt.Println(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /external/docker/commands/status.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/docker/docker/client" 10 | "github.com/logrusorgru/aurora" 11 | 12 | "github.com/pygmystack/pygmy/external/docker/setup" 13 | "github.com/pygmystack/pygmy/internal/runtime/docker" 14 | runtimecontainers "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 15 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/networks" 16 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/volumes" 17 | "github.com/pygmystack/pygmy/internal/utils/color" 18 | "github.com/pygmystack/pygmy/internal/utils/endpoint" 19 | "github.com/pygmystack/pygmy/internal/utils/resolv" 20 | ) 21 | 22 | // Status will show the state of all the things Pygmy manages. 23 | func Status(ctx context.Context, cli *client.Client, c setup.Config) { 24 | setup.Setup(ctx, cli, &c) 25 | checks, _ := setup.DryRun(ctx, cli, &c) 26 | agentPresent := false 27 | 28 | if len(checks) > 0 { 29 | for _, check := range checks { 30 | c.JSONStatus.PortAvailability = append(c.JSONStatus.PortAvailability, check.Message) 31 | } 32 | } 33 | 34 | // Ensure the services struct is not nil. 35 | c.JSONStatus.Services = make(map[string]setup.StatusJSONStatus) 36 | 37 | Containers, _ := runtimecontainers.List(ctx, cli) 38 | for _, Container := range Containers { 39 | if Container.Labels["pygmy.enable"] == "true" || Container.Labels["pygmy.enable"] == "1" { 40 | Service := c.Services[strings.Trim(Container.Names[0], "/")] 41 | if s, _ := Service.Status(ctx, cli); s { 42 | name, _ := Service.GetFieldString(ctx, cli, "name") 43 | enabled, _ := Service.GetFieldBool(ctx, cli, "enable") 44 | discrete, _ := Service.GetFieldBool(ctx, cli, "discrete") 45 | purpose, _ := Service.GetFieldString(ctx, cli, "purpose") 46 | if name != "" { 47 | if purpose == "sshagent" { 48 | agentPresent = true 49 | } 50 | if enabled && !discrete && name != "" { 51 | if s, _ := Service.Status(ctx, cli); s { 52 | c.JSONStatus.Services[name] = setup.StatusJSONStatus{ 53 | Container: name, 54 | ImageRef: Service.Image, 55 | State: true, 56 | } 57 | } else { 58 | c.JSONStatus.Services[name] = setup.StatusJSONStatus{ 59 | Container: name, 60 | ImageRef: Service.Image, 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | for _, Service := range c.Services { 70 | if s, _ := Service.Status(ctx, cli); !s { 71 | name, _ := Service.GetFieldString(ctx, cli, "name") 72 | discrete, _ := Service.GetFieldBool(ctx, cli, "discrete") 73 | if !discrete { 74 | c.JSONStatus.Services[name] = setup.StatusJSONStatus{ 75 | Container: name, 76 | ImageRef: Service.Image, 77 | } 78 | } 79 | } 80 | } 81 | 82 | for _, Network := range c.Networks { 83 | for _, Container := range Network.Containers { 84 | if x, _ := networks.Connected(ctx, cli, Network.Name, Container.Name); !x { 85 | c.JSONStatus.Networks = append(c.JSONStatus.Networks, fmt.Sprintf("%s is not connected to the network %s", Container.Name, Network.Name)) 86 | } else { 87 | c.JSONStatus.Networks = append(c.JSONStatus.Networks, fmt.Sprintf("%s is connected to the network %s", Container.Name, Network.Name)) 88 | } 89 | } 90 | } 91 | 92 | for _, resolver := range c.Resolvers { 93 | r := resolv.Resolv{Name: resolver.Name, Data: resolver.Data, Folder: resolver.Folder, File: resolver.File} 94 | if s := r.Status(&docker.Params{Domain: c.Domain}); s { 95 | c.JSONStatus.Resolvers = append(c.JSONStatus.Resolvers, fmt.Sprintf("Resolv %s is properly connected", resolver.Name)) 96 | } else { 97 | c.JSONStatus.Resolvers = append(c.JSONStatus.Resolvers, fmt.Sprintf("Resolv %s is not properly connected", resolver.Name)) 98 | } 99 | } 100 | 101 | for _, volume := range c.Volumes { 102 | if s, _ := volumes.Exists(ctx, cli, volume.Name); s { 103 | c.JSONStatus.Volumes = append(c.JSONStatus.Volumes, fmt.Sprintf("Volume %s has been created", volume.Name)) 104 | } else { 105 | c.JSONStatus.Volumes = append(c.JSONStatus.Volumes, fmt.Sprintf("Volume %s has not been created", volume.Name)) 106 | } 107 | } 108 | 109 | // Show ssh-keys in the agent 110 | if agentPresent { 111 | for _, v := range c.Services { 112 | purpose, _ := v.GetFieldString(ctx, cli, "purpose") 113 | if purpose == "sshagent" { 114 | l, _ := runtimecontainers.Exec(ctx, cli, v.Config.Labels["pygmy.name"], "ssh-add -l") 115 | // Remove \u0000 & \u0001 from output messages. 116 | output := strings.ReplaceAll(string(l), "\u0000", "") 117 | output = strings.ReplaceAll(output, "\u0001", "") 118 | output = strings.Trim(output, "\n") 119 | c.JSONStatus.SSHMessages = append(c.JSONStatus.SSHMessages, output) 120 | } 121 | } 122 | } 123 | 124 | // List out all running projects to get their URL. 125 | var urls []string 126 | 127 | for _, Container := range c.Services { 128 | Status, _ := Container.Status(ctx, cli) 129 | url, _ := Container.GetFieldString(ctx, cli, "url") 130 | if url != "" && Status { 131 | urls = append(urls, url) 132 | } 133 | } 134 | 135 | containers, _ := runtimecontainers.List(ctx, cli) 136 | for _, container := range containers { 137 | if container.State == "running" && !strings.Contains(fmt.Sprint(container.Names), "amazeeio") { 138 | obj, _ := runtimecontainers.Inspect(ctx, cli, container.ID) 139 | vars := obj.Config.Env 140 | for _, v := range vars { 141 | // Look for the environment variable $LAGOON_ROUTE. 142 | if strings.Contains(v, "LAGOON_ROUTE=") { 143 | url := strings.TrimPrefix(v, "LAGOON_ROUTE=") 144 | if !strings.HasPrefix(url, "http") && !strings.HasPrefix(url, "https") { 145 | url = "http://" + url 146 | } 147 | urls = append(urls, url) 148 | } 149 | } 150 | } 151 | } 152 | 153 | cleanurls := setup.Unique(urls) 154 | for _, url := range cleanurls { 155 | result := endpoint.Validate(url) 156 | c.JSONStatus.URLValidations = append(c.JSONStatus.URLValidations, setup.StatusJSONURLValidation{ 157 | Endpoint: url, 158 | Success: result, 159 | }) 160 | } 161 | 162 | if c.JSONFormat { 163 | PrintStatusJSON(c) 164 | return 165 | } 166 | 167 | PrintStatusHumanReadable(c) 168 | 169 | } 170 | 171 | func PrintStatusJSON(c setup.Config) { 172 | jsonData, _ := json.Marshal(c.JSONStatus) 173 | fmt.Println(string(jsonData)) 174 | 175 | } 176 | func PrintStatusHumanReadable(c setup.Config) { 177 | for _, v := range c.JSONStatus.PortAvailability { 178 | if strings.Contains(v, "is not able to start on port") { 179 | color.Print(aurora.Red(fmt.Sprintf("[ ] %s\n", v))) 180 | } else { 181 | color.Print(aurora.Green(fmt.Sprintf("[*] %s\n", v))) 182 | } 183 | } 184 | 185 | for k, v := range c.JSONStatus.Services { 186 | if v.State { 187 | color.Print(aurora.Green(fmt.Sprintf("[*] %s: Running as container %s\n", k, v.Container))) 188 | } else { 189 | color.Print(aurora.Red(fmt.Sprintf("[ ] %s is not running\n", k))) 190 | } 191 | } 192 | 193 | for _, v := range c.JSONStatus.Resolvers { 194 | if strings.Contains(v, "not properly connected") { 195 | color.Print(aurora.Red(fmt.Sprintf("[ ] %s\n", v))) 196 | } else { 197 | color.Print(aurora.Green(fmt.Sprintf("[*] %s\n", v))) 198 | } 199 | } 200 | 201 | for _, v := range c.JSONStatus.Networks { 202 | if strings.Contains(v, "is not connected to network") { 203 | color.Print(aurora.Red(fmt.Sprintf("[ ] %s\n", v))) 204 | } else { 205 | color.Print(aurora.Green(fmt.Sprintf("[*] %s\n", v))) 206 | } 207 | } 208 | 209 | for _, v := range c.JSONStatus.Volumes { 210 | if strings.Contains(v, "has not been created") { 211 | color.Print(aurora.Red(fmt.Sprintf("[ ] %s\n", v))) 212 | } else { 213 | color.Print(aurora.Green(fmt.Sprintf("[*] %s\n", v))) 214 | } 215 | } 216 | 217 | for _, v := range c.JSONStatus.SSHMessages { 218 | fmt.Printf("%s\n", v) 219 | } 220 | 221 | for _, v := range c.JSONStatus.URLValidations { 222 | if v.Success { 223 | fmt.Printf(" - %s\n", v.Endpoint) 224 | } else { 225 | fmt.Printf(" ! %s\n", v.Endpoint) 226 | } 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /external/docker/commands/stop.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pygmystack/pygmy/external/docker/setup" 7 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 8 | ) 9 | 10 | // Stop will bring pygmy down safely 11 | func Stop(c setup.Config) error { 12 | cli, ctx, err := internals.NewClient() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | setup.Setup(ctx, cli, &c) 18 | 19 | for _, Service := range c.Services { 20 | enabled, _ := Service.GetFieldBool(ctx, cli, "enable") 21 | if enabled { 22 | e := Service.Stop(ctx, cli) 23 | if e != nil { 24 | fmt.Println(e) 25 | } 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /external/docker/commands/up.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | . "github.com/logrusorgru/aurora" 9 | 10 | "github.com/pygmystack/pygmy/external/docker/setup" 11 | "github.com/pygmystack/pygmy/internal/runtime/docker" 12 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 13 | runtimecontainers "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 14 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/networks" 15 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/volumes" 16 | "github.com/pygmystack/pygmy/internal/utils/color" 17 | "github.com/pygmystack/pygmy/internal/utils/endpoint" 18 | ) 19 | 20 | // Up will bring Pygmy up. 21 | func Up(c setup.Config) error { 22 | cli, ctx, err := internals.NewClient() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | setup.Setup(ctx, cli, &c) 28 | checks, _ := setup.DryRun(ctx, cli, &c) 29 | agentPresent := false 30 | 31 | foundIssues := 0 32 | for _, check := range checks { 33 | if !check.State { 34 | fmt.Println(check.Message) 35 | foundIssues++ 36 | } 37 | } 38 | if foundIssues > 0 { 39 | fmt.Println("Please address the above issues before you attempt to start Pygmy again.") 40 | os.Exit(1) 41 | } 42 | 43 | for _, volume := range c.Volumes { 44 | if s, _ := volumes.Exists(ctx, cli, volume.Name); !s { 45 | _, err := volumes.Create(ctx, cli, volume) 46 | if err == nil { 47 | color.Print(Green(fmt.Sprintf("Created volume %s\n", volume.Name))) 48 | } else { 49 | fmt.Println(err) 50 | } 51 | } else { 52 | color.Print(Green(fmt.Sprintf("Already created volume %s\n", volume.Name))) 53 | } 54 | } 55 | 56 | // Maps are... bad for predictable sequencing. 57 | // Look over the sorted slice and start them in 58 | // alphabetical order - so that one can configure 59 | // an ssh-agent like amazeeio-ssh-agent. 60 | for _, s := range c.SortedServices { 61 | service := c.Services[s] 62 | enabled, _ := service.GetFieldBool(ctx, cli, "enable") 63 | purpose, _ := service.GetFieldString(ctx, cli, "purpose") 64 | name, _ := service.GetFieldString(ctx, cli, "name") 65 | 66 | // Do not show or add keys: 67 | if enabled && purpose != "addkeys" { 68 | 69 | if se := service.Setup(ctx, cli); se == nil { 70 | fmt.Print(Green(fmt.Sprintf("Successfully pulled %s\n", service.Config.Image))) 71 | } 72 | if status, _ := service.Status(ctx, cli); !status { 73 | if ce := service.Create(ctx, cli); ce != nil { 74 | // If the status is false but the container is already created, we can ignore that error. 75 | if !strings.Contains(ce.Error(), "namespace is already taken") { 76 | fmt.Printf("Failed to create %s: %s\n", Red(name), ce) 77 | } 78 | } 79 | if se := service.Start(ctx, cli); se == nil { 80 | fmt.Print(Green(fmt.Sprintf("Successfully started %s\n", name))) 81 | } else { 82 | fmt.Printf("Failed to start %s: %s\n", Red(name), se) 83 | } 84 | } else { 85 | fmt.Print(Green(fmt.Sprintf("Already started %s\n", name))) 86 | } 87 | } 88 | 89 | // If one or more agent was found: 90 | if purpose == "sshagent" { 91 | agentPresent = true 92 | } 93 | } 94 | 95 | // Docker network(s) creation 96 | for _, Network := range c.Networks { 97 | if Network.Name != "" { 98 | netVal, _ := networks.Status(ctx, cli, Network.Name) 99 | if !netVal { 100 | if err := networks.Create(ctx, cli, &Network); err == nil { 101 | color.Print(Green(fmt.Sprintf("Successfully created network %s\n", Network.Name))) 102 | } else { 103 | color.Print(Red(fmt.Sprintf("Could not create network %s\n", Network.Name))) 104 | } 105 | } 106 | } 107 | } 108 | 109 | // Container network connection(s) 110 | for _, s := range c.SortedServices { 111 | service := c.Services[s] 112 | name, nameErr := service.GetFieldString(ctx, cli, "name") 113 | // If the network is configured at the container level, connect it. 114 | if Network, _ := service.GetFieldString(ctx, cli, "network"); Network != "" && nameErr == nil { 115 | if s, _ := networks.Connected(ctx, cli, Network, name); !s { 116 | if s := networks.Connect(ctx, cli, Network, name); s == nil { 117 | color.Print(Green(fmt.Sprintf("Successfully connected %s to %s\n", name, Network))) 118 | } else { 119 | discrete, _ := service.GetFieldBool(ctx, cli, "discrete") 120 | if !discrete { 121 | color.Print(Red(fmt.Sprintf("Could not connect %s to %s\n", name, Network))) 122 | } 123 | } 124 | } else { 125 | color.Print(Green(fmt.Sprintf("Already connected %s to %s\n", name, Network))) 126 | } 127 | } 128 | } 129 | 130 | for _, resolver := range c.Resolvers { 131 | if !resolver.Status(&docker.Params{Domain: c.Domain}) { 132 | resolver.Configure(&docker.Params{Domain: c.Domain}) 133 | } 134 | } 135 | 136 | // Add ssh-keys to the agent 137 | if agentPresent { 138 | for _, v := range c.Keys { 139 | if e := SshKeyAdd(c, v.Path); e != nil { 140 | color.Print(Red(fmt.Sprintf("%v\n", e))) 141 | } 142 | } 143 | } 144 | 145 | for _, service := range c.Services { 146 | name, _ := service.GetFieldString(ctx, cli, "name") 147 | url, _ := service.GetFieldString(ctx, cli, "url") 148 | if s, _ := service.Status(ctx, cli); s && url != "" { 149 | endpoint.Validate(url) 150 | if r := endpoint.Validate(url); r { 151 | fmt.Printf(" - %v (%v)\n", url, name) 152 | } else { 153 | fmt.Printf(" ! %v (%v)\n", url, name) 154 | } 155 | } 156 | } 157 | 158 | // List out all running projects to get their URL. 159 | containers, _ := runtimecontainers.List(ctx, cli) 160 | var urls []string 161 | for _, container := range containers { 162 | if container.State == "running" && !strings.Contains(fmt.Sprint(container.Names), "amazeeio") { 163 | obj, _ := runtimecontainers.Inspect(ctx, cli, container.ID) 164 | vars := obj.Config.Env 165 | for _, v := range vars { 166 | // Look for the environment variable $LAGOON_ROUTE. 167 | if strings.Contains(v, "LAGOON_ROUTE=") { 168 | url := strings.TrimPrefix(v, "LAGOON_ROUTE=") 169 | if !strings.HasPrefix(url, "http") && !strings.HasPrefix(url, "https") { 170 | url = "http://" + url 171 | } 172 | urls = append(urls, url) 173 | } 174 | } 175 | } 176 | } 177 | 178 | cleanurls := setup.Unique(urls) 179 | for _, url := range cleanurls { 180 | endpoint.Validate(url) 181 | if r := endpoint.Validate(url); r { 182 | fmt.Printf(" - %v\n", url) 183 | } else { 184 | fmt.Printf(" ! %v\n", url) 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /external/docker/commands/update.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pygmystack/pygmy/external/docker/setup" 8 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 9 | runtimeimages "github.com/pygmystack/pygmy/internal/runtime/docker/internals/images" 10 | ) 11 | 12 | // Update will update the images for all configured services. 13 | func Update(c setup.Config) error { 14 | cli, ctx, err := internals.NewClient() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | // Import the configuration. 20 | setup.Setup(ctx, cli, &c) 21 | 22 | // Loop over services. 23 | for s := range c.Services { 24 | 25 | // Pull the image. 26 | service := c.Services[s] 27 | purpose, _ := service.GetFieldString(ctx, cli, "purpose") 28 | var result string 29 | var err error 30 | if purpose == "" || purpose == "sshagent" { 31 | result, err = runtimeimages.Pull(ctx, cli, service.Config.Image) 32 | if err == nil { 33 | fmt.Println(result) 34 | } else { 35 | fmt.Println(err) 36 | } 37 | } 38 | 39 | // If the service is running, restart it. 40 | if s, _ := service.Status(ctx, cli); s && !strings.Contains(result, "is up to date") { 41 | var e error 42 | e = service.Stop(ctx, cli) 43 | if e != nil { 44 | fmt.Println(e) 45 | } 46 | if s, _ := service.Status(ctx, cli); !s { 47 | e = service.Start(ctx, cli) 48 | if e != nil { 49 | fmt.Println(e) 50 | } 51 | } 52 | } 53 | } 54 | 55 | images, _ := runtimeimages.List(ctx, cli) 56 | for _, image := range images { 57 | for _, tag := range image.RepoTags { 58 | if strings.Contains(tag, "uselagoon") { 59 | result, err := runtimeimages.Pull(ctx, cli, tag) 60 | if err == nil { 61 | fmt.Println(result) 62 | } 63 | } 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /external/docker/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pygmystack/pygmy/external/docker/setup" 6 | "runtime/debug" 7 | ) 8 | 9 | // Version describes which version of Pygmy is running. 10 | func Version(c setup.Config) { 11 | info, _ := debug.ReadBuildInfo() 12 | 13 | if info.Main.Version == "(devel)" { 14 | fmt.Println("Development version") 15 | return 16 | } 17 | 18 | fmt.Printf("Pygmy %s\n", info.Main.Version) 19 | } 20 | -------------------------------------------------------------------------------- /external/docker/setup/dryrun.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/docker/docker/client" 10 | ) 11 | 12 | // CompatibilityCheck is a struct of fields associated to reporting of 13 | // a result state. 14 | type CompatibilityCheck struct { 15 | State bool `yaml:"value"` 16 | Message string `yaml:"string"` 17 | } 18 | 19 | // DryRun will check for. It is here to check for port compatibility before 20 | // Pygmy attempts to start any containers and provide the user with a report. 21 | func DryRun(ctx context.Context, cli *client.Client, c *Config) ([]CompatibilityCheck, error) { 22 | 23 | messages := []CompatibilityCheck{} 24 | 25 | for _, Service := range c.Services { 26 | name, _ := Service.GetFieldString(ctx, cli, "name") 27 | enabled, _ := Service.GetFieldBool(ctx, cli, "enable") 28 | if enabled { 29 | if s, _ := Service.Status(ctx, cli); !s { 30 | for PortBinding, Ports := range Service.HostConfig.PortBindings { 31 | if strings.Contains(string(PortBinding), "tcp") { 32 | for _, Port := range Ports { 33 | p := fmt.Sprint(Port.HostPort) 34 | conn, err := net.Dial("tcp", "localhost:"+p) 35 | if conn != nil { 36 | if e := conn.Close(); e != nil { 37 | fmt.Println(e) 38 | } 39 | } 40 | if err != nil { 41 | messages = append(messages, CompatibilityCheck{ 42 | State: true, 43 | Message: fmt.Sprintf("%v is able to start on port %v", name, p), 44 | }) 45 | } else { 46 | conn, err := net.Listen("tcp", ":"+p) 47 | if conn != nil { 48 | conn.Close() 49 | } 50 | if err != nil { 51 | messages = append(messages, CompatibilityCheck{ 52 | State: false, 53 | Message: fmt.Sprintf("%v is not able to start on port %v: %v", name, p, err), 54 | }) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | return messages, nil 65 | } 66 | -------------------------------------------------------------------------------- /external/docker/setup/network.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/imdario/mergo" 7 | 8 | networktypes "github.com/docker/docker/api/types/network" 9 | ) 10 | 11 | // mergeNetwork will merge two Network objects. 12 | func mergeNetwork(destination networktypes.Inspect, src *networktypes.Inspect) (*networktypes.Inspect, error) { 13 | if err := mergo.Merge(&destination, src, mergo.WithOverride); err != nil { 14 | fmt.Println(err) 15 | return src, err 16 | } 17 | return &destination, nil 18 | } 19 | 20 | // GetNetwork will return a network from the configuration. 21 | // This merges the information down to return the object, so it cannot be implemented in the networks package. 22 | func GetNetwork(s networktypes.Inspect, c networktypes.Inspect) networktypes.Inspect { 23 | Network, _ := mergeNetwork(s, &c) 24 | return *Network 25 | } 26 | -------------------------------------------------------------------------------- /external/docker/setup/service.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/imdario/mergo" 7 | 8 | dockerruntime "github.com/pygmystack/pygmy/internal/runtime/docker" 9 | ) 10 | 11 | // mergeService will merge two Service objects. 12 | func mergeService(destination dockerruntime.Service, src *dockerruntime.Service) (*dockerruntime.Service, error) { 13 | if err := mergo.Merge(&destination, src, mergo.WithOverride); err != nil { 14 | fmt.Println(err) 15 | return src, err 16 | } 17 | return &destination, nil 18 | } 19 | 20 | // GetService will return a service from the configuration. 21 | // This merges the information down to return the object, so it cannot be implemented in the another package. 22 | func GetService(s dockerruntime.Service, c dockerruntime.Service) dockerruntime.Service { 23 | Service, _ := mergeService(s, &c) 24 | return *Service 25 | } 26 | -------------------------------------------------------------------------------- /external/docker/setup/setup.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "sort" 9 | "strings" 10 | 11 | networktypes "github.com/docker/docker/api/types/network" 12 | "github.com/docker/docker/api/types/volume" 13 | "github.com/docker/docker/client" 14 | "github.com/spf13/viper" 15 | 16 | dockerruntime "github.com/pygmystack/pygmy/internal/runtime/docker" 17 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/volumes" 18 | "github.com/pygmystack/pygmy/internal/service/docker/dnsmasq" 19 | "github.com/pygmystack/pygmy/internal/service/docker/haproxy" 20 | "github.com/pygmystack/pygmy/internal/service/docker/mailhog" 21 | "github.com/pygmystack/pygmy/internal/service/docker/ssh/agent" 22 | "github.com/pygmystack/pygmy/internal/service/docker/ssh/key" 23 | "github.com/pygmystack/pygmy/internal/utils/network/docker" 24 | "github.com/pygmystack/pygmy/internal/utils/resolv" 25 | ) 26 | 27 | // ImportDefaults is an exported function which allows third-party applications 28 | // to provide their own *Service and integrate it with their application so 29 | // that Pygmy is more extendable via API. It's here so that we have one common 30 | // import functionality that respects the users' decision to import config 31 | // defaults in a centralized way. 32 | func ImportDefaults(ctx context.Context, cli *client.Client, c *Config, service string, importer dockerruntime.Service) bool { 33 | if _, ok := c.Services[service]; ok { 34 | 35 | container := c.Services[service] 36 | 37 | // If configuration has a value for the defaults label 38 | if val, ok := container.Config.Labels["pygmy.defaults"]; ok { 39 | if val == "1" || val == "true" { 40 | // Clear destination Service to a new nil value. 41 | c.Services[service] = dockerruntime.Service{} 42 | // Import the provided Service to the map entry. 43 | c.Services[service] = GetService(importer, c.Services[service]) 44 | // This is now successful, so return true. 45 | return true 46 | } 47 | } 48 | 49 | // If container has a value for the defaults label 50 | if defaultsNeeded, _ := container.GetFieldBool(ctx, cli, "defaults"); defaultsNeeded { 51 | c.Services[service] = GetService(importer, c.Services[service]) 52 | return true 53 | } 54 | 55 | // If default configuration has a value for the defaults label 56 | if val, ok := importer.Config.Labels["pygmy.defaults"]; ok { 57 | if val == "1" || val == "true" { 58 | c.Services[service] = GetService(importer, c.Services[service]) 59 | return true 60 | } 61 | } 62 | } else { 63 | if defaultsNeeded, _ := importer.GetFieldBool(ctx, cli, "defaults"); defaultsNeeded { 64 | c.Services[service] = GetService(importer, c.Services[service]) 65 | return true 66 | } 67 | } 68 | 69 | return false 70 | } 71 | 72 | // Setup holds the core of configuration management with Pygmy. 73 | // It will merge in all the configurations and provide defaults. 74 | func Setup(ctx context.Context, cli *client.Client, c *Config) { 75 | 76 | // All Viper API calls for default values go here. 77 | 78 | // Set default value for default inheritance: 79 | viper.SetDefault("defaults", true) 80 | 81 | // Set the default domain. 82 | viper.SetDefault("domain", "docker.amazee.io") 83 | if c.Domain == "" { 84 | c.Domain = viper.GetString("domain") 85 | } 86 | 87 | // Resolvers don't have hard defaults defined which 88 | // are mergable. So we set them in viper before 89 | // unmarshalling the config so that config specified 90 | // will override the default, but the default won't 91 | // be overridden if it's not specified. 92 | if viper.GetBool("defaults") { 93 | 94 | var ResolvMacOS = resolv.Resolv{ 95 | Data: fmt.Sprintf("# Generated by amazeeio pygmy\nnameserver 127.0.0.1\ndomain %s\nport 6053\n", c.Domain), 96 | Enabled: true, 97 | File: c.Domain, 98 | Folder: "/etc/resolver", 99 | Name: "MacOS Resolver", 100 | } 101 | 102 | var ResolvLinux = resolv.Resolv{ 103 | Data: fmt.Sprintf("# Generated by amazeeio pygmy\n[Resolve]\nDNS=127.0.0.1:6053\nDomains=~%s\n", c.Domain), 104 | Enabled: true, 105 | File: fmt.Sprintf("%s.conf", c.Domain), 106 | Folder: "/usr/lib/systemd/resolved.conf.d", 107 | Name: "Linux Resolver", 108 | } 109 | 110 | if runtime.GOOS == "darwin" { 111 | viper.SetDefault("resolvers", []resolv.Resolv{ 112 | ResolvMacOS, 113 | }) 114 | } else if runtime.GOOS == "linux" { 115 | viper.SetDefault("resolvers", []resolv.Resolv{ 116 | ResolvLinux, 117 | }) 118 | } else if runtime.GOOS == "windows" { 119 | viper.SetDefault("resolvers", []resolv.Resolv{}) 120 | } 121 | } 122 | 123 | e := viper.Unmarshal(&c) 124 | 125 | if e != nil { 126 | fmt.Println(e) 127 | } 128 | 129 | if c.Defaults { 130 | 131 | // If Services have been provided in complete or partially, 132 | // this will override the defaults allowing any value to 133 | // be changed by the user in the configuration file ~/.pygmy.yml 134 | if len(c.Services) == 0 { 135 | c.Services = make(map[string]dockerruntime.Service, 6) 136 | } 137 | 138 | ImportDefaults(ctx, cli, c, "amazeeio-ssh-agent", agent.New()) 139 | ImportDefaults(ctx, cli, c, "amazeeio-ssh-agent-add-key", key.NewAdder()) 140 | ImportDefaults(ctx, cli, c, "amazeeio-dnsmasq", dnsmasq.New(&dockerruntime.Params{Domain: c.Domain})) 141 | ImportDefaults(ctx, cli, c, "amazeeio-haproxy", haproxy.New(&dockerruntime.Params{Domain: c.Domain})) 142 | ImportDefaults(ctx, cli, c, "amazeeio-mailhog", mailhog.New(&dockerruntime.Params{Domain: c.Domain})) 143 | 144 | // Disable Resolvers if needed. 145 | if c.ResolversDisabled { 146 | c.Resolvers = nil 147 | } 148 | 149 | // We need Port 80 to be configured by default. 150 | // If a port on amazeeio-haproxy isn't explicitly declared, 151 | // then we should set this value. This is far more creative 152 | // than needed, so feel free to revisit if you can compile it. 153 | if c.Services["amazeeio-haproxy"].HostConfig.PortBindings == nil { 154 | c.Services["amazeeio-haproxy"] = GetService(haproxy.NewDefaultPorts(), c.Services["amazeeio-haproxy"]) 155 | } 156 | 157 | // It's sensible to use the same logic for port 1025. 158 | // If a user needs to configure it, the default value should not be set also. 159 | if c.Services["amazeeio-mailhog"].HostConfig.PortBindings == nil { 160 | c.Services["amazeeio-mailhog"] = GetService(mailhog.NewDefaultPorts(), c.Services["amazeeio-mailhog"]) 161 | } 162 | 163 | // Ensure Networks has a at least a zero value. 164 | // We should provide defaults for amazeeio-network when no value is provided. 165 | if c.Networks == nil { 166 | c.Networks = make(map[string]networktypes.Inspect) 167 | c.Networks["amazeeio-network"] = GetNetwork(docker.New(), c.Networks["amazeeio-network"]) 168 | } 169 | 170 | // Ensure Volumes has a at least a zero value. 171 | if c.Volumes == nil { 172 | c.Volumes = make(map[string]volume.Volume) 173 | } 174 | 175 | for _, v := range c.Volumes { 176 | // Get the potentially existing volume: 177 | c.Volumes[v.Name], _ = volumes.Get(ctx, cli, v.Name) 178 | // Merge the volume with the provided configuration: 179 | c.Volumes[v.Name] = GetVolume(c.Volumes[v.Name], c.Volumes[v.Name]) 180 | } 181 | } 182 | 183 | // Mandatory validation check. 184 | for id, service := range c.Services { 185 | if name, err := service.GetFieldString(ctx, cli, "name"); err != nil && name != "" { 186 | fmt.Printf("service '%v' does not have have a value for label 'pygmy.name'\n", id) 187 | os.Exit(2) 188 | } 189 | if service.Config.Image == "" { 190 | fmt.Printf("service '%v' does not have have a value for {{.Config.Image}}\n", id) 191 | os.Exit(2) 192 | } 193 | } 194 | 195 | // Image overrides when specified. 196 | for name, service := range c.Services { 197 | if service.Image != "" { 198 | // Re-apply the image reference. 199 | service.Config.Image = service.Image 200 | } else { 201 | // Sync the strings when unspecified. 202 | service.Image = service.Config.Image 203 | } 204 | // Replace the Service object. 205 | c.Services[name] = service 206 | } 207 | 208 | // Determine the slice of sorted services 209 | c.SortedServices = GetServicesSorted(ctx, cli, c) 210 | } 211 | 212 | // GetServicesSorted will return a list of services as plain text. 213 | // due to some weirdness the ssh agent must be the first value. 214 | func GetServicesSorted(ctx context.Context, cli *client.Client, c *Config) []string { 215 | 216 | SortedServices := make([]string, 0) 217 | SSHAgentServiceName := "" 218 | 219 | // Do not add ssh-agent in the first run. 220 | for key, service := range c.Services { 221 | name, _ := service.GetFieldString(ctx, cli, "name") 222 | purpose, _ := service.GetFieldString(ctx, cli, "purpose") 223 | weight, _ := service.GetFieldInt(ctx, cli, "weight") 224 | if purpose == "sshagent" { 225 | SSHAgentServiceName = name 226 | } else { 227 | SortedServices = append(SortedServices, fmt.Sprintf("%06d|%v", weight, key)) 228 | } 229 | } 230 | 231 | // Alphabetical sorting. 232 | sort.Strings(SortedServices) 233 | 234 | // Strip the ordering prefix from the service name 235 | for n, v := range SortedServices { 236 | SortedServices[n] = strings.Split(v, "|")[1] 237 | } 238 | 239 | if SSHAgentServiceName != "" { 240 | SortedServices = append([]string{SSHAgentServiceName}, SortedServices...) 241 | } 242 | return SortedServices 243 | 244 | } 245 | -------------------------------------------------------------------------------- /external/docker/setup/setup_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | 8 | "github.com/pygmystack/pygmy/external/docker/setup" 9 | "github.com/pygmystack/pygmy/internal/runtime/docker" 10 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 11 | ) 12 | 13 | // Tests the setup process. 14 | func TestSetup(t *testing.T) { 15 | // Get our configuration object 16 | c := &setup.Config{ 17 | Services: map[string]docker.Service{ 18 | "amazeeio-dnsmasq": { 19 | // Set an override config value so it can be tested. 20 | Image: "example-amazeeio-dnsmasq", 21 | }, 22 | "amazeeio-mailhog": { 23 | // Set an override config value so it can be tested. 24 | Image: "example-amazeeio-mailhog", 25 | }, 26 | }, 27 | } 28 | 29 | cli, ctx, err := internals.NewClient() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | setup.Setup(ctx, cli, c) 35 | c.SortedServices = setup.GetServicesSorted(ctx, cli, c) 36 | 37 | Convey("Setup Tests", t, func() { 38 | // SSH Agent must be 5 items long by default. 39 | So(c.SortedServices, ShouldHaveLength, 5) 40 | // SSH Agent must be the first item in the sorted list. 41 | So(c.SortedServices[0], ShouldEqual, "amazeeio-ssh-agent") 42 | // Test sorting result. 43 | So(c.SortedServices[1], ShouldEqual, "amazeeio-dnsmasq") 44 | So(c.SortedServices[2], ShouldEqual, "amazeeio-haproxy") 45 | So(c.SortedServices[3], ShouldEqual, "amazeeio-mailhog") 46 | So(c.SortedServices[4], ShouldEqual, "amazeeio-ssh-agent-add-key") 47 | // Test Image Override configuration. 48 | So(c.Services["amazeeio-dnsmasq"].Image, ShouldEqual, "example-amazeeio-dnsmasq") 49 | So(c.Services["amazeeio-dnsmasq"].Config.Image, ShouldEqual, "example-amazeeio-dnsmasq") 50 | So(c.Services["amazeeio-haproxy"].Image, ShouldEqual, "pygmystack/haproxy") 51 | So(c.Services["amazeeio-haproxy"].Config.Image, ShouldEqual, "pygmystack/haproxy") 52 | So(c.Services["amazeeio-mailhog"].Image, ShouldEqual, "example-amazeeio-mailhog") 53 | So(c.Services["amazeeio-mailhog"].Config.Image, ShouldEqual, "example-amazeeio-mailhog") 54 | So(c.Services["amazeeio-ssh-agent"].Image, ShouldEqual, "pygmystack/ssh-agent") 55 | So(c.Services["amazeeio-ssh-agent"].Config.Image, ShouldEqual, "pygmystack/ssh-agent") 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /external/docker/setup/types.go: -------------------------------------------------------------------------------- 1 | // Package setup is a package which exposes the commands externally to the compiled binaries. 2 | package setup 3 | 4 | import ( 5 | networktypes "github.com/docker/docker/api/types/network" 6 | volumetypes "github.com/docker/docker/api/types/volume" 7 | dockerruntime "github.com/pygmystack/pygmy/internal/runtime/docker" 8 | "github.com/pygmystack/pygmy/internal/utils/resolv" 9 | ) 10 | 11 | // Config is a struct of configurable options which can 12 | // be passed to package library to configure logic for 13 | // continued abstraction. 14 | type Config struct { 15 | // Keys are the paths to the Keys which should be added. 16 | Keys []Key `yaml:"keys"` 17 | 18 | // Domain is the default domain suffix to use. 19 | Domain string `yaml:"domain"` 20 | 21 | // Services is a []model.Service for an index of all Services. 22 | Services map[string]dockerruntime.Service `yaml:"services"` 23 | 24 | SortedServices []string 25 | 26 | // Networks is for network configuration 27 | Networks map[string]networktypes.Inspect `yaml:"networks"` 28 | 29 | // NoDefaults will prevent default configuration items. 30 | Defaults bool 31 | 32 | // JSONFormat indicates the `status` command should print to stdout in JSON format. 33 | JSONFormat bool 34 | 35 | // JSONStatus contains JSON status content. 36 | JSONStatus StatusJSON 37 | 38 | // ResolversDisabled will disable the creation of any resolv configurations. 39 | ResolversDisabled bool `yaml:"resolversDisabled"` 40 | 41 | // Resolvers is for all resolvers 42 | Resolvers []resolv.Resolv `yaml:"resolvers"` 43 | 44 | // Volumes will ensure names volumes are created 45 | Volumes map[string]volumetypes.Volume 46 | } 47 | 48 | type StatusJSON struct { 49 | PortAvailability []string `json:"port_availability"` 50 | Services map[string]StatusJSONStatus `json:"service_status"` 51 | Networks []string `json:"networks"` 52 | Resolvers []string `json:"resolvers"` 53 | Volumes []string `json:"volumes"` 54 | SSHMessages []string `json:"ssh_messages"` 55 | URLValidations []StatusJSONURLValidation `json:"url_validations"` 56 | } 57 | 58 | type StatusJSONURLValidation struct { 59 | Endpoint string `json:"endpoint"` 60 | Success bool `json:"success"` 61 | } 62 | 63 | type StatusJSONStatus struct { 64 | Container string `json:"container"` 65 | ImageRef string `json:"image"` 66 | State bool `json:"running"` 67 | } 68 | 69 | // Key is a struct with SSH key details. 70 | type Key struct { 71 | Path string `yaml:"path"` 72 | } 73 | -------------------------------------------------------------------------------- /external/docker/setup/utils.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | // Unique will return a slice with duplicates 4 | // removed. It performs a similar function to 5 | // the linux program `uniq` 6 | func Unique(stringSlice []string) []string { 7 | m := make(map[string]bool) 8 | for _, item := range stringSlice { 9 | if _, ok := m[item]; !ok { 10 | m[item] = true 11 | } 12 | } 13 | 14 | var result []string 15 | for item := range m { 16 | result = append(result, item) 17 | } 18 | return result 19 | } 20 | -------------------------------------------------------------------------------- /external/docker/setup/volume.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | 6 | volumetypes "github.com/docker/docker/api/types/volume" 7 | "github.com/imdario/mergo" 8 | ) 9 | 10 | // mergeVolume will merge two Volume objects. 11 | func mergeVolume(destination volumetypes.Volume, src *volumetypes.Volume) (*volumetypes.Volume, error) { 12 | if err := mergo.Merge(&destination, src, mergo.WithOverride); err != nil { 13 | fmt.Println(err) 14 | return src, err 15 | } 16 | return &destination, nil 17 | } 18 | 19 | // GetVolume will return a volume from the configuration. 20 | // This merges the information down to return the object, so it cannot be implemented in the volumes package. 21 | func GetVolume(s volumetypes.Volume, c volumetypes.Volume) volumetypes.Volume { 22 | Volume, _ := mergeVolume(s, &c) 23 | return *Volume 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pygmystack/pygmy 2 | 3 | go 1.21.0 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/containerd/platforms v0.2.1 8 | github.com/docker/docker v28.1.1+incompatible 9 | github.com/docker/go-connections v0.5.0 10 | github.com/ghodss/yaml v1.0.0 11 | github.com/imdario/mergo v0.3.16 12 | github.com/logrusorgru/aurora v2.0.3+incompatible 13 | github.com/mattn/go-colorable v0.1.14 14 | github.com/mitchellh/go-homedir v1.1.0 15 | github.com/opencontainers/image-spec v1.1.1 16 | github.com/smartystreets/goconvey v1.8.1 17 | github.com/spf13/cobra v1.8.1 18 | github.com/spf13/viper v1.20.1 19 | github.com/stretchr/testify v1.10.0 20 | golang.org/x/crypto v0.38.0 21 | golang.org/x/term v0.32.0 22 | ) 23 | 24 | require ( 25 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 26 | github.com/Microsoft/go-winio v0.6.2 // indirect 27 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 28 | github.com/containerd/log v0.1.0 // indirect 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/distribution/reference v0.6.0 // indirect 31 | github.com/docker/go-units v0.5.0 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/fsnotify/fsnotify v1.8.0 // indirect 34 | github.com/go-logr/logr v1.4.2 // indirect 35 | github.com/go-logr/stdr v1.2.2 // indirect 36 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 37 | github.com/gogo/protobuf v1.3.2 // indirect 38 | github.com/gopherjs/gopherjs v1.17.2 // indirect 39 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect 40 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 41 | github.com/jtolds/gls v4.20.0+incompatible // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/moby/docker-image-spec v1.3.1 // indirect 44 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 45 | github.com/moby/term v0.5.0 // indirect 46 | github.com/morikuni/aec v1.0.0 // indirect 47 | github.com/opencontainers/go-digest v1.0.0 // indirect 48 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 51 | github.com/rogpeppe/go-internal v1.11.0 // indirect 52 | github.com/sagikazarmark/locafero v0.7.0 // indirect 53 | github.com/sirupsen/logrus v1.9.3 // indirect 54 | github.com/smarty/assertions v1.15.0 // indirect 55 | github.com/sourcegraph/conc v0.3.0 // indirect 56 | github.com/spf13/afero v1.12.0 // indirect 57 | github.com/spf13/cast v1.7.1 // indirect 58 | github.com/spf13/pflag v1.0.6 // indirect 59 | github.com/subosito/gotenv v1.6.0 // indirect 60 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 61 | go.opentelemetry.io/otel v1.29.0 // indirect 62 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect 63 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect 64 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 65 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 66 | go.uber.org/multierr v1.11.0 // indirect 67 | golang.org/x/net v0.38.0 // indirect 68 | golang.org/x/sys v0.33.0 // indirect 69 | golang.org/x/text v0.25.0 // indirect 70 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | gopkg.in/yaml.v3 v3.0.1 // indirect 73 | gotest.tools/v3 v3.5.1 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/client.go: -------------------------------------------------------------------------------- 1 | package internals 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/docker/docker/client" 8 | 9 | containercontext "github.com/pygmystack/pygmy/internal/runtime/docker/internals/context" 10 | ) 11 | 12 | func NewClient() (*client.Client, context.Context, error) { 13 | ctx := context.Background() 14 | clientOpts := []client.Opt{ 15 | client.WithAPIVersionNegotiation(), 16 | } 17 | if os.Getenv("DOCKER_HOST") != "" { 18 | clientOpts = append(clientOpts, client.FromEnv) 19 | } else if currentDockerHost, err := containercontext.CurrentDockerHost(); err != nil { 20 | return nil, nil, err 21 | } else if currentDockerHost != "" { 22 | clientOpts = append(clientOpts, client.WithHost(currentDockerHost)) 23 | } 24 | cli, err := client.NewClientWithOpts(clientOpts...) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | return cli, ctx, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/containers/container.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/containerd/platforms" 12 | "github.com/docker/docker/api/types" 13 | "github.com/docker/docker/api/types/container" 14 | containertypes "github.com/docker/docker/api/types/container" 15 | networktypes "github.com/docker/docker/api/types/network" 16 | "github.com/docker/docker/client" 17 | v1 "github.com/opencontainers/image-spec/specs-go/v1" 18 | ) 19 | 20 | // Stop will stop the container. 21 | func Stop(ctx context.Context, client *client.Client, name string) error { 22 | timeout := 10 23 | err := client.ContainerStop(ctx, name, containertypes.StopOptions{Timeout: &timeout}) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | // Kill will kill the container. 31 | func Kill(ctx context.Context, client *client.Client, name string) error { 32 | err := client.ContainerKill(ctx, name, "") 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | // Remove will remove the container. 40 | // It will not remove the image. 41 | func Remove(ctx context.Context, client *client.Client, id string) error { 42 | err := client.ContainerRemove(ctx, id, containertypes.RemoveOptions{}) 43 | if err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | // Inspect will return the full container object. 50 | func Inspect(ctx context.Context, client *client.Client, container string) (container.InspectResponse, error) { 51 | return client.ContainerInspect(ctx, container) 52 | } 53 | 54 | // Exec will run a command in a Docker container and return the output. 55 | func Exec(ctx context.Context, client *client.Client, container string, command string) ([]byte, error) { 56 | rst, err := client.ContainerExecCreate(ctx, container, containertypes.ExecOptions{ 57 | AttachStdout: true, 58 | AttachStderr: true, 59 | Cmd: strings.Split(command, " ")}) 60 | 61 | if err != nil { 62 | return []byte{}, err 63 | } 64 | 65 | response, err := client.ContainerExecAttach(context.Background(), rst.ID, containertypes.ExecAttachOptions{}) 66 | 67 | if err != nil { 68 | return []byte{}, err 69 | } 70 | 71 | data, _ := io.ReadAll(response.Reader) 72 | defer response.Close() 73 | return data, nil 74 | 75 | } 76 | 77 | // List will return a slice of containers 78 | func List(ctx context.Context, client *client.Client) ([]container.Summary, error) { 79 | containers, err := client.ContainerList(ctx, containertypes.ListOptions{ 80 | All: true, 81 | }) 82 | if err != nil { 83 | return []container.Summary{}, err 84 | } 85 | 86 | return containers, nil 87 | } 88 | 89 | // Create will create a container, but will not run it. 90 | func Create(ctx context.Context, client *client.Client, ID string, config containertypes.Config, hostconfig containertypes.HostConfig, networkconfig networktypes.NetworkingConfig) (containertypes.CreateResponse, error) { 91 | platform := platforms.Normalize(v1.Platform{ 92 | Architecture: runtime.GOARCH, 93 | OS: "linux", 94 | }) 95 | resp, err := client.ContainerCreate(ctx, &config, &hostconfig, &networkconfig, &platform, ID) 96 | if err != nil { 97 | return containertypes.CreateResponse{}, err 98 | } 99 | return resp, err 100 | } 101 | 102 | // Attach will return an attached response to a container. 103 | func Attach(ctx context.Context, client *client.Client, ID string, options containertypes.AttachOptions) (types.HijackedResponse, error) { 104 | resp, err := client.ContainerAttach(ctx, ID, options) 105 | if err != nil { 106 | return types.HijackedResponse{}, err 107 | } 108 | return resp, err 109 | } 110 | 111 | // Start will run an existing container. 112 | func Start(ctx context.Context, client *client.Client, ID string, options containertypes.StartOptions) error { 113 | return client.ContainerStart(ctx, ID, containertypes.StartOptions{}) 114 | } 115 | 116 | // Wait will wait for the specificied container condition. 117 | func Wait(ctx context.Context, client *client.Client, ID string, condition containertypes.WaitCondition) error { 118 | statusCh, errCh := client.ContainerWait(ctx, ID, condition) 119 | select { 120 | case err := <-errCh: 121 | if err != nil { 122 | return err 123 | } 124 | case <-statusCh: 125 | return nil 126 | } 127 | 128 | return nil 129 | } 130 | 131 | // Logs will synchronously (blocking, non-concurrently) print 132 | // logs to stdout and stderr, useful for quick containers with a small amount 133 | // of output which are expected to exit quickly. 134 | func Logs(ctx context.Context, client *client.Client, ID string) ([]byte, error) { 135 | b, e := client.ContainerLogs(ctx, ID, containertypes.LogsOptions{ 136 | ShowStdout: true, 137 | ShowStderr: true, 138 | }) 139 | 140 | if e != nil { 141 | return []byte{}, e 142 | } 143 | 144 | buf := new(bytes.Buffer) 145 | if _, f := buf.ReadFrom(b); f != nil { 146 | fmt.Println(f) 147 | } 148 | 149 | return buf.Bytes(), nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type DockerConfig struct { 12 | CurrentContext string `json:"currentContext"` 13 | } 14 | 15 | type DockerContextManifest struct { 16 | Name string 17 | Endpoints map[string]struct { 18 | Host string 19 | } 20 | } 21 | 22 | func filePathInHomeDir(elem ...string) (string, error) { 23 | // Find home directory. 24 | home, err := os.UserHomeDir() 25 | if err != nil { 26 | return "", err 27 | } 28 | return filepath.Join(append([]string{home}, elem...)...), nil 29 | } 30 | 31 | func currentContext() (string, error) { 32 | configPath, err := filePathInHomeDir(".docker", "config.json") 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | configBytes, err := os.ReadFile(configPath) 38 | if err != nil { 39 | if os.IsNotExist(err) { 40 | return "", nil 41 | } 42 | return "", errors.New(err.(*os.PathError).Error()) 43 | } 44 | 45 | dockerConfig := DockerConfig{} 46 | err = json.Unmarshal(configBytes, &dockerConfig) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | return dockerConfig.CurrentContext, nil 52 | } 53 | 54 | func endpointFromContext(context string) (string, error) { 55 | manifestDir, err := filePathInHomeDir(".docker", "contexts", "meta") 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | var contextManifest DockerContextManifest 61 | 62 | err = filepath.WalkDir(manifestDir, func(path string, d fs.DirEntry, err error) error { 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if d.Name() != "meta.json" { 68 | return nil 69 | } 70 | manifestBytes, err := os.ReadFile(path) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | manifest := DockerContextManifest{} 76 | err = json.Unmarshal(manifestBytes, &manifest) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | if manifest.Name == context { 82 | contextManifest = manifest 83 | } 84 | return nil 85 | }) 86 | 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | return contextManifest.Endpoints["docker"].Host, nil 92 | } 93 | 94 | func CurrentDockerHost() (string, error) { 95 | dockerCurrentContext, err := currentContext() 96 | if err != nil { 97 | return "", err 98 | } 99 | 100 | currentDockerHost := "" 101 | if dockerCurrentContext != "" { 102 | currentDockerHost, err = endpointFromContext(dockerCurrentContext) 103 | if err != nil { 104 | return "", err 105 | } 106 | } 107 | return currentDockerHost, nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/context/context_test.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // TestCurrentDockerHost will test the CurrentDockerHost function. 11 | func TestCurrentDockerHost(t *testing.T) { 12 | _, err := CurrentDockerHost() 13 | assert.Nil(t, err) 14 | } 15 | 16 | // TestCurrentContext will test the currentContext function. 17 | func TestCurrentContext(t *testing.T) { 18 | _, err := currentContext() 19 | assert.NoError(t, err) 20 | } 21 | 22 | // TestEndpointFromContext will test the endpointFromContext function. 23 | func TestEndpointFromContext(t *testing.T) { 24 | manifestDir, err := filePathInHomeDir(".docker", "contexts", "meta") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | // To prevent flakey tests, we will test if the file exists and dynamically 29 | // assert the result depending on the outcomes. 30 | if _, err = os.Stat(manifestDir); os.IsNotExist(err) { 31 | _, err = endpointFromContext("") 32 | assert.NotNil(t, err) 33 | } else { 34 | _, err = endpointFromContext("") 35 | assert.Nil(t, err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/images/image.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "regexp" 9 | "strings" 10 | 11 | img "github.com/docker/docker/api/types/image" 12 | "github.com/docker/docker/client" 13 | 14 | "github.com/pygmystack/pygmy/internal/utils/endpoint" 15 | ) 16 | 17 | // Remove will remove an image from the registry. 18 | // Pygmy doesn't need this, but it serves as a tool for testing this package. 19 | func Remove(ctx context.Context, cli *client.Client, id string) ([]img.DeleteResponse, error) { 20 | images, err := cli.ImageRemove(ctx, id, img.RemoveOptions{}) 21 | if err != nil { 22 | return []img.DeleteResponse{}, err 23 | } 24 | return images, nil 25 | } 26 | 27 | // List will return a slice of Docker images. 28 | func List(ctx context.Context, cli *client.Client) ([]img.Summary, error) { 29 | images, err := cli.ImageList(ctx, img.ListOptions{ 30 | All: true, 31 | }) 32 | if err != nil { 33 | return []img.Summary{}, err 34 | } 35 | return images, nil 36 | 37 | } 38 | 39 | // Pull will pull a Docker image into the daemon. 40 | func Pull(ctx context.Context, cli *client.Client, image string) (string, error) { 41 | { 42 | 43 | // To support image references from external sources to docker.io we need to check 44 | // and validate the image reference for all known cases of validity. 45 | 46 | if m, _ := regexp.MatchString("^(([a-zA-Z0-9_.-]+)[/]([a-zA-Z0-9_.-]+)[/]([a-zA-Z0-9_.-]+)[:]([a-zA-Z0-9_.-]+))$", image); m { 47 | // URL was provided (in full), but the tag was provided. 48 | // For this, we do not alter the value provided. 49 | // Examples: 50 | // - quay.io/pygmystack/pygmy:latest 51 | image = fmt.Sprintf("%v", image) 52 | } else if m, _ := regexp.MatchString("^(([a-zA-Z0-9_.-]+)[/]([a-zA-Z0-9_.-]+)[/]([a-zA-Z0-9_.-]+))$", image); m { 53 | // URL was provided (in full), but the tag was not provided. 54 | // For this, we do not alter the value provided. 55 | // Examples: 56 | // - quay.io/pygmystack/pygmy 57 | image = fmt.Sprintf("%v:latest", image) 58 | } else if m, _ := regexp.MatchString("^(([a-zA-Z0-9_.-]+)[/]([a-zA-Z0-9_.-]+)[:]([a-zA-Z0-9_.-]+))$", image); m { 59 | // URL was not provided (in full), but the tag was provided. 60 | // For this, we prepend 'docker.io/' to the reference. 61 | // Examples: 62 | // - pygmystack/pygmy:latest 63 | image = fmt.Sprintf("docker.io/%v", image) 64 | } else if m, _ := regexp.MatchString("^(([a-zA-Z0-9_.-]+)[/]([a-zA-Z0-9_.-]+))$", image); m { 65 | // URL was not provided (in full), but the tag was not provided. 66 | // For this, we prepend 'docker.io/' to the reference. 67 | // Examples: 68 | // - pygmystack/pygmy 69 | image = fmt.Sprintf("docker.io/%v:latest", image) 70 | } else if m, _ := regexp.MatchString("^(([a-zA-Z0-9_.-]+)[:]([a-zA-Z0-9_.-]+))$", image); m { 71 | // Library image was provided with tag identifier. 72 | // For this, we prepend 'docker.io/' to the reference. 73 | // Examples: 74 | // - pygmy:latest 75 | image = fmt.Sprintf("docker.io/%v", image) 76 | } else if m, _ := regexp.MatchString("^([a-zA-Z0-9_.-]+)$", image); m { 77 | // Library image was provided without tag identifier. 78 | // For this, we prepend 'docker.io/' to the reference. 79 | // Examples: 80 | // - pygmy 81 | image = fmt.Sprintf("docker.io/%v:latest", image) 82 | } else { 83 | // Validation not successful 84 | return image, fmt.Errorf("error: regexp validation for %v failed", image) 85 | } 86 | } 87 | 88 | // DockerHub Registry causes a stack trace fatal error when unavailable. 89 | // We can check for this and report back, handling it gracefully and 90 | // tell the user the service is down momentarily, and to try again shortly. 91 | if strings.HasPrefix(image, "docker.io") { 92 | if s := endpoint.Validate("https://registry-1.docker.io/v2/"); !s { 93 | return image, fmt.Errorf("cannot reach the Docker Hub Registry, please try again in a few minutes") 94 | } 95 | } 96 | 97 | data, err := cli.ImagePull(ctx, image, img.PullOptions{}) 98 | d := json.NewDecoder(data) 99 | 100 | type Event struct { 101 | Status string `json:"status"` 102 | Error string `json:"error"` 103 | Progress string `json:"progress"` 104 | ProgressDetail struct { 105 | Current int `json:"current"` 106 | Total int `json:"total"` 107 | } `json:"progressDetail"` 108 | } 109 | 110 | var event *Event 111 | if err == nil { 112 | for { 113 | if err := d.Decode(&event); err != nil { 114 | if err == io.EOF { 115 | break 116 | } 117 | 118 | panic(err) 119 | } 120 | } 121 | 122 | if event != nil { 123 | if strings.Contains(event.Status, "Downloaded newer image") { 124 | return fmt.Sprintf("Successfully pulled %v", image), nil 125 | } 126 | 127 | if strings.Contains(event.Status, "Image is up to date") { 128 | return fmt.Sprintf("Image %v is up to date", image), nil 129 | } 130 | } 131 | 132 | return event.Status, nil 133 | } 134 | 135 | if strings.Contains(err.Error(), "pull access denied") { 136 | return fmt.Sprintf("Error trying to update image %v: pull access denied", image), nil 137 | } 138 | 139 | return image, nil 140 | } 141 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/images/image_test.go: -------------------------------------------------------------------------------- 1 | package images 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "testing" 7 | 8 | "github.com/docker/docker/client" 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 12 | ) 13 | 14 | func testSetup() (context.Context, *client.Client) { 15 | cli, ctx, err := internals.NewClient() 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | return ctx, cli 21 | } 22 | 23 | // TestPullAndList will test the image functionality for pulling and listing a Docker image. 24 | func TestPullAndList(t *testing.T) { 25 | ctx, cli := testSetup() 26 | 27 | id := "nginx:latest" 28 | 29 | // Remove the image from the registry. 30 | // We specifically do not want check this error. 31 | _, _ = Remove(ctx, cli, id) 32 | 33 | // Pull the image into the registry. 34 | pullResponse, err := Pull(ctx, cli, id) 35 | assert.NoError(t, err) 36 | 37 | // Ensure the output from this test contains some expected text. 38 | assert.Contains(t, pullResponse, "docker.io/nginx:latest") 39 | 40 | // List the images in the registry. 41 | list, err := List(ctx, cli) 42 | assert.NoError(t, err) 43 | 44 | // Check for the image in the registry. 45 | foundNginxImage := false 46 | for _, img := range list { 47 | if slices.Contains(img.RepoTags, "nginx:latest") { 48 | foundNginxImage = true 49 | } 50 | } 51 | assert.True(t, foundNginxImage) 52 | 53 | // Clean-up for this test. 54 | _, err = Remove(ctx, cli, id) 55 | assert.NoError(t, err) 56 | } 57 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/networks/network.go: -------------------------------------------------------------------------------- 1 | package networks 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | networktypes "github.com/docker/docker/api/types/network" 8 | "github.com/docker/docker/client" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 11 | ) 12 | 13 | // Create is an abstraction layer on top of the Docker API call 14 | // which will create a Docker network using a specified configuration. 15 | func Create(ctx context.Context, cli *client.Client, network *networktypes.Inspect) error { 16 | netVal, _ := Status(ctx, cli, network.Name) 17 | if netVal { 18 | return fmt.Errorf("docker network %v already exists", network.Name) 19 | } 20 | 21 | config := networktypes.CreateOptions{ 22 | Driver: network.Driver, 23 | EnableIPv6: &network.EnableIPv6, 24 | IPAM: &network.IPAM, 25 | Internal: network.Internal, 26 | Attachable: network.Attachable, 27 | Options: network.Options, 28 | Labels: network.Labels, 29 | } 30 | _, err := cli.NetworkCreate(ctx, network.Name, config) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // Remove will attempt to remove a Docker network 39 | // and will not apply force to removal. 40 | func Remove(ctx context.Context, cli *client.Client, network string) error { 41 | err := cli.NetworkRemove(ctx, network) 42 | if err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | // Status will identify if a network with a 49 | // specified name is present been created and return a boolean. 50 | func Status(ctx context.Context, cli *client.Client, network string) (bool, error) { 51 | networks, err := cli.NetworkList(ctx, networktypes.ListOptions{}) 52 | if err != nil { 53 | return false, err 54 | } 55 | 56 | for _, n := range networks { 57 | if n.Name == network { 58 | return true, nil 59 | } 60 | } 61 | 62 | return false, nil 63 | } 64 | 65 | // Get will use the Docker API to retrieve a Docker network 66 | // which has a given name. 67 | func Get(ctx context.Context, cli *client.Client, name string) (networktypes.Inspect, error) { 68 | networks, err := cli.NetworkList(ctx, networktypes.ListOptions{}) 69 | if err != nil { 70 | return networktypes.Inspect{}, err 71 | } 72 | for _, network := range networks { 73 | if val, ok := network.Labels["pygmy.name"]; ok { 74 | if val == name { 75 | return network, nil 76 | } 77 | } 78 | } 79 | return networktypes.Inspect{}, nil 80 | } 81 | 82 | // Connect will connect a container to a network. 83 | func Connect(ctx context.Context, cli *client.Client, network string, containerName string) error { 84 | e := cli.NetworkConnect(ctx, network, containerName, nil) 85 | if e != nil { 86 | return e 87 | } 88 | return nil 89 | } 90 | 91 | // Connected will check if a container is connected to a network. 92 | func Connected(ctx context.Context, cli *client.Client, network string, containerName string) (bool, error) { 93 | // Reset network state: 94 | c, _ := containers.List(ctx, cli) 95 | for d := range c { 96 | if c[d].Labels["pygmy.name"] == containerName { 97 | for net := range c[d].NetworkSettings.Networks { 98 | if net == network { 99 | return true, nil 100 | } 101 | } 102 | } 103 | } 104 | return false, fmt.Errorf("network was found without the container connected") 105 | } 106 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/networks/network_test.go: -------------------------------------------------------------------------------- 1 | package networks 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/docker/api/types/network" 12 | "github.com/docker/docker/client" 13 | "github.com/stretchr/testify/assert" 14 | 15 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 16 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 17 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/images" 18 | ) 19 | 20 | // sampleData will generate sample data for use in each test. 21 | func sampleData() (container.Config, container.HostConfig, network.NetworkingConfig) { 22 | config := container.Config{ 23 | Image: "nginx", 24 | } 25 | hostConfig := container.HostConfig{} 26 | networkConfig := network.NetworkingConfig{} 27 | 28 | return config, hostConfig, networkConfig 29 | } 30 | 31 | // randomString will generate a random string. 32 | func randomString(length int) string { 33 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 34 | source := rand.NewSource(time.Now().UnixNano()) 35 | r := rand.New(source) 36 | 37 | result := make([]byte, length) 38 | for i := range result { 39 | result[i] = charset[r.Intn(len(charset))] 40 | } 41 | return string(result) 42 | } 43 | 44 | // testSetup will prepare the client for each test. 45 | func testSetup() (context.Context, *client.Client) { 46 | cli, ctx, err := internals.NewClient() 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | return ctx, cli 52 | } 53 | 54 | // TestCreate will test the network creation API request. 55 | func TestCreate(t *testing.T) { 56 | ctx, cli := testSetup() 57 | id := fmt.Sprintf("testNetwork-%s", randomString(10)) 58 | 59 | network := &network.Inspect{ 60 | Name: id, 61 | Labels: map[string]string{ 62 | "pygmy.name": id, 63 | }, 64 | } 65 | 66 | err := Create(ctx, cli, network) 67 | assert.NoError(t, err) 68 | 69 | err = Remove(ctx, cli, id) 70 | assert.NoError(t, err) 71 | } 72 | 73 | // TestRemove will test the network removal API request. 74 | func TestRemove(t *testing.T) { 75 | ctx, cli := testSetup() 76 | id := fmt.Sprintf("testNetwork-%s", randomString(10)) 77 | 78 | network := &network.Inspect{ 79 | Name: id, 80 | Labels: map[string]string{ 81 | "pygmy.name": id, 82 | }, 83 | } 84 | 85 | err := Create(ctx, cli, network) 86 | assert.NoError(t, err) 87 | 88 | err = Remove(ctx, cli, id) 89 | assert.NoError(t, err) 90 | } 91 | 92 | // TestStatus will test the network status API request. 93 | func TestStatus(t *testing.T) { 94 | ctx, cli := testSetup() 95 | id := fmt.Sprintf("testNetwork-%s", randomString(10)) 96 | 97 | network := &network.Inspect{ 98 | Name: id, 99 | Labels: map[string]string{ 100 | "pygmy.name": id, 101 | }, 102 | } 103 | 104 | err := Create(ctx, cli, network) 105 | assert.NoError(t, err) 106 | 107 | status, err := Status(ctx, cli, id) 108 | assert.NoError(t, err) 109 | assert.True(t, status) 110 | 111 | err = Remove(ctx, cli, id) 112 | assert.NoError(t, err) 113 | } 114 | 115 | // TestGet will test the network getter API request. 116 | func TestGet(t *testing.T) { 117 | ctx, cli := testSetup() 118 | id := fmt.Sprintf("testNetwork-%s", randomString(10)) 119 | 120 | network := &network.Inspect{ 121 | Name: id, 122 | Labels: map[string]string{ 123 | "pygmy.name": id, 124 | }, 125 | } 126 | 127 | err := Create(ctx, cli, network) 128 | assert.NoError(t, err) 129 | 130 | obj, err := Get(ctx, cli, id) 131 | assert.NoError(t, err) 132 | assert.Equal(t, network.Name, obj.Name) 133 | 134 | err = Remove(ctx, cli, id) 135 | assert.NoError(t, err) 136 | } 137 | 138 | // TestConnect will test the network connection API request. 139 | func TestConnect(t *testing.T) { 140 | ctx, cli := testSetup() 141 | config, hostconfig, networkconfig := sampleData() 142 | id := fmt.Sprintf("testNetwork-%s", randomString(10)) 143 | containerName := fmt.Sprintf("testContainer-%s", randomString(10)) 144 | 145 | config.Labels = map[string]string{ 146 | "pygmy.name": containerName, 147 | } 148 | 149 | // Pull the image 150 | _, err := images.Pull(ctx, cli, config.Image) 151 | assert.NoError(t, err) 152 | 153 | // Create a container to stop. 154 | _, err = containers.Create(ctx, cli, containerName, config, hostconfig, networkconfig) 155 | assert.NoError(t, err) 156 | 157 | // Start the container to stop. 158 | err = containers.Start(ctx, cli, containerName, container.StartOptions{}) 159 | assert.NoError(t, err) 160 | 161 | network := &network.Inspect{ 162 | Name: id, 163 | Labels: map[string]string{ 164 | "pygmy.name": id, 165 | }, 166 | } 167 | 168 | err = Create(ctx, cli, network) 169 | assert.NoError(t, err) 170 | 171 | err = Connect(ctx, cli, id, containerName) 172 | assert.NoError(t, err) 173 | 174 | // Start the container to stop. 175 | err = containers.Stop(ctx, cli, containerName) 176 | assert.NoError(t, err) 177 | 178 | // Start the container to stop. 179 | err = containers.Remove(ctx, cli, containerName) 180 | assert.NoError(t, err) 181 | 182 | err = Remove(ctx, cli, id) 183 | assert.NoError(t, err) 184 | } 185 | 186 | // TestConnect will test the validity of a network connection with a container. 187 | func TestConnected(t *testing.T) { 188 | ctx, cli := testSetup() 189 | config, hostconfig, networkconfig := sampleData() 190 | id := fmt.Sprintf("testNetwork-%s", randomString(10)) 191 | containerName := fmt.Sprintf("testContainer-%s", randomString(10)) 192 | 193 | config.Labels = map[string]string{ 194 | "pygmy.name": containerName, 195 | } 196 | 197 | // Pull the image 198 | _, err := images.Pull(ctx, cli, config.Image) 199 | assert.NoError(t, err) 200 | 201 | // Create a container to stop. 202 | _, err = containers.Create(ctx, cli, containerName, config, hostconfig, networkconfig) 203 | assert.NoError(t, err) 204 | 205 | // Start the container to stop. 206 | err = containers.Start(ctx, cli, containerName, container.StartOptions{}) 207 | assert.NoError(t, err) 208 | 209 | network := &network.Inspect{ 210 | Name: id, 211 | Labels: map[string]string{ 212 | "pygmy.name": id, 213 | }, 214 | } 215 | 216 | err = Create(ctx, cli, network) 217 | assert.NoError(t, err) 218 | 219 | err = Connect(ctx, cli, id, containerName) 220 | assert.NoError(t, err) 221 | 222 | connection, err := Connected(ctx, cli, id, containerName) 223 | assert.NoError(t, err) 224 | assert.True(t, connection) 225 | 226 | // Start the container to stop. 227 | err = containers.Stop(ctx, cli, containerName) 228 | assert.NoError(t, err) 229 | 230 | // Start the container to stop. 231 | err = containers.Remove(ctx, cli, containerName) 232 | assert.NoError(t, err) 233 | 234 | err = Remove(ctx, cli, id) 235 | assert.NoError(t, err) 236 | } 237 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/volumes/volume.go: -------------------------------------------------------------------------------- 1 | package volumes 2 | 3 | import ( 4 | "context" 5 | "github.com/docker/docker/api/types/volume" 6 | "github.com/docker/docker/client" 7 | ) 8 | 9 | // Exists will check if a Docker volume has been created. 10 | func Exists(ctx context.Context, cli *client.Client, volume string) (bool, error) { 11 | _, _, err := cli.VolumeInspectWithRaw(ctx, volume) 12 | if err != nil { 13 | return false, err 14 | } 15 | 16 | return true, nil 17 | } 18 | 19 | // Get will return the full contents of a types.Volume from the API. 20 | func Get(ctx context.Context, cli *client.Client, name string) (volume.Volume, error) { 21 | volumes, err := cli.VolumeList(ctx, volume.ListOptions{}) 22 | if err != nil { 23 | return volume.Volume{ 24 | Name: name, 25 | }, err 26 | } 27 | 28 | for _, volume := range volumes.Volumes { 29 | if volume.Name == name { 30 | return *volume, nil 31 | } 32 | } 33 | 34 | return volume.Volume{ 35 | Name: name, 36 | }, nil 37 | } 38 | 39 | // Create will create a Docker Volume as configured. 40 | func Create(ctx context.Context, cli *client.Client, volumeInput volume.Volume) (volume.Volume, error) { 41 | return cli.VolumeCreate(ctx, volume.CreateOptions{ 42 | Driver: volumeInput.Driver, 43 | DriverOpts: volumeInput.Options, 44 | Labels: volumeInput.Labels, 45 | Name: volumeInput.Name, 46 | }) 47 | } 48 | 49 | // Remove will remove a Docker volume, which will be used exclusively for testing. 50 | func Remove(ctx context.Context, cli *client.Client, volume string) error { 51 | return cli.VolumeRemove(ctx, volume, false) 52 | } 53 | -------------------------------------------------------------------------------- /internal/runtime/docker/internals/volumes/volume_test.go: -------------------------------------------------------------------------------- 1 | package volumes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | volume2 "github.com/docker/docker/api/types/volume" 7 | "github.com/stretchr/testify/assert" 8 | "math/rand" 9 | "testing" 10 | "time" 11 | 12 | "github.com/docker/docker/client" 13 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 14 | ) 15 | 16 | // testSetup will prepare the client for each test. 17 | func testSetup() (context.Context, *client.Client) { 18 | cli, ctx, err := internals.NewClient() 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | return ctx, cli 24 | } 25 | 26 | // randomString will generate a random string. 27 | func randomString(length int) string { 28 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 29 | source := rand.NewSource(time.Now().UnixNano()) 30 | r := rand.New(source) 31 | 32 | result := make([]byte, length) 33 | for i := range result { 34 | result[i] = charset[r.Intn(len(charset))] 35 | } 36 | return string(result) 37 | } 38 | 39 | // TestCreate will test the API call for volume creation. 40 | func TestCreate(t *testing.T) { 41 | ctx, cli := testSetup() 42 | volumeName := fmt.Sprintf("testVolume-%s", randomString(10)) 43 | volume := volume2.Volume{ 44 | Name: volumeName, 45 | } 46 | 47 | // Create a volume. 48 | _, err := Create(ctx, cli, volume) 49 | assert.NoError(t, err) 50 | 51 | // Remove the volume. 52 | err = Remove(ctx, cli, volumeName) 53 | assert.NoError(t, err) 54 | } 55 | 56 | // TestRemove will test the API call for volume removal. 57 | func TestRemove(t *testing.T) { 58 | ctx, cli := testSetup() 59 | volumeName := fmt.Sprintf("testVolume-%s", randomString(10)) 60 | volume := volume2.Volume{ 61 | Name: volumeName, 62 | } 63 | 64 | // Create a volume. 65 | _, err := Create(ctx, cli, volume) 66 | assert.NoError(t, err) 67 | 68 | // Remove the volume. 69 | err = Remove(ctx, cli, volumeName) 70 | assert.NoError(t, err) 71 | } 72 | 73 | // TestGet will test the API call for volume getting. 74 | func TestGet(t *testing.T) { 75 | ctx, cli := testSetup() 76 | volumeName := fmt.Sprintf("testVolume-%s", randomString(10)) 77 | volume := volume2.Volume{ 78 | Name: volumeName, 79 | } 80 | 81 | // Create a volume. 82 | _, err := Create(ctx, cli, volume) 83 | assert.NoError(t, err) 84 | 85 | // Get a volume 86 | v, err := Get(ctx, cli, volumeName) 87 | assert.NoError(t, err) 88 | assert.Equal(t, volumeName, v.Name) 89 | 90 | // Remove the volume. 91 | err = Remove(ctx, cli, volumeName) 92 | assert.NoError(t, err) 93 | } 94 | 95 | // TestExists will test the API call for volume existing. 96 | func TestExists(t *testing.T) { 97 | ctx, cli := testSetup() 98 | volumeName := fmt.Sprintf("testVolume-%s", randomString(10)) 99 | volume := volume2.Volume{ 100 | Name: volumeName, 101 | } 102 | 103 | // Create a volume. 104 | _, err := Create(ctx, cli, volume) 105 | assert.NoError(t, err) 106 | 107 | // Check if the volume exists. 108 | exists, err := Exists(ctx, cli, volumeName) 109 | assert.NoError(t, err) 110 | assert.True(t, exists) 111 | 112 | // Remove the volume. 113 | err = Remove(ctx, cli, volumeName) 114 | assert.NoError(t, err) 115 | } 116 | -------------------------------------------------------------------------------- /internal/runtime/docker/types.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | containertypes "github.com/docker/docker/api/types/container" 5 | networktypes "github.com/docker/docker/api/types/network" 6 | ) 7 | 8 | type Service struct { 9 | Config containertypes.Config 10 | HostConfig containertypes.HostConfig 11 | Image string `yaml:"image"` 12 | NetworkConfig networktypes.NetworkingConfig 13 | } 14 | 15 | // Params is an arbitrary struct to pass around configuration from the top 16 | // level to the lowest level - such as variable input to one of the 17 | // containers. 18 | type Params struct { 19 | // Domain is the target domain for Pygmy to use. 20 | Domain string 21 | } 22 | -------------------------------------------------------------------------------- /internal/runtime/docker/utils.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/docker/docker/client" 9 | ) 10 | 11 | // SetField will set a pygmy label to be equal to the string equal of 12 | // an interface{}, even if it already exists. It should not matter if 13 | // this container is running or not. 14 | func (Service *Service) SetField(ctx context.Context, cli *client.Client, name string, value interface{}) error { 15 | if _, ok := Service.Config.Labels["pygmy."+fmt.Sprint(name)]; !ok { 16 | // 17 | } else { 18 | old, _ := Service.GetFieldString(ctx, cli, name) 19 | Service.Config.Labels["pygmy."+name] = fmt.Sprint(value) 20 | new, _ := Service.GetFieldString(ctx, cli, name) 21 | 22 | if old == new { 23 | return fmt.Errorf("tag was not set") 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // GetFieldString will get and return a tag on the service using the pygmy 31 | // convention ("pygmy.*") and return it as a string. 32 | func (Service *Service) GetFieldString(ctx context.Context, cli *client.Client, field string) (string, error) { 33 | 34 | f := fmt.Sprintf("pygmy.%v", field) 35 | 36 | if labels, running := Service.Labels(ctx, cli); running == nil { 37 | if val, ok := labels[f]; ok { 38 | return val, nil 39 | } 40 | } 41 | 42 | if val, ok := Service.Config.Labels[f]; ok { 43 | return val, nil 44 | } 45 | 46 | return "", fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) 47 | } 48 | 49 | // GetFieldInt will get and return a tag on the service using the pygmy 50 | // convention ("pygmy.*") and return it as an int. 51 | func (Service *Service) GetFieldInt(ctx context.Context, cli *client.Client, field string) (int, error) { 52 | 53 | f := fmt.Sprintf("pygmy.%v", field) 54 | 55 | if labels, running := Service.Labels(ctx, cli); running == nil { 56 | if val, ok := labels[f]; ok { 57 | i, e := strconv.ParseInt(val, 10, 10) 58 | if e != nil { 59 | return 0, e 60 | } 61 | return int(i), nil 62 | } 63 | } 64 | 65 | if val, ok := Service.Config.Labels[f]; ok { 66 | i, e := strconv.ParseInt(val, 10, 10) 67 | if e != nil { 68 | return 0, e 69 | } 70 | return int(i), nil 71 | } 72 | 73 | return 0, fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) 74 | } 75 | 76 | // GetFieldBool will get and return a tag on the service using the pygmy 77 | // convention ("pygmy.*") and return it as a bool. 78 | func (Service *Service) GetFieldBool(ctx context.Context, cli *client.Client, field string) (bool, error) { 79 | 80 | f := fmt.Sprintf("pygmy.%v", field) 81 | 82 | if labels, running := Service.Labels(ctx, cli); running == nil { 83 | if Service.Config.Labels[f] == labels[f] { 84 | if val, ok := labels[f]; ok { 85 | if val == "true" { 86 | return true, nil 87 | } else if val == "false" { 88 | return false, nil 89 | } 90 | } 91 | } 92 | } 93 | 94 | if val, ok := Service.Config.Labels[f]; ok { 95 | if val == "true" || val == "1" { 96 | return true, nil 97 | } else if val == "false" || val == "0" { 98 | return false, nil 99 | } 100 | } 101 | 102 | return false, fmt.Errorf("could not find field 'pygmy.%v' on service using image %v?", field, Service.Config.Image) 103 | } 104 | -------------------------------------------------------------------------------- /internal/runtime/podman/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pygmystack/pygmy/2cb96865d6205cf331171954e73cc4310ee2f6e4/internal/runtime/podman/.gitkeep -------------------------------------------------------------------------------- /internal/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/docker/docker/client" 7 | ) 8 | 9 | // ServiceRuntime is the definition of a Container Runtime for compatability with Pygmy. 10 | type ServiceRuntime interface { 11 | Setup(ctx context.Context, cli *client.Client) error 12 | Start(ctx context.Context, cli *client.Client) error 13 | Create(ctx context.Context, cli *client.Client) error 14 | Status(ctx context.Context, cli *client.Client) (bool, error) 15 | Labels(ctx context.Context, cli *client.Client) (map[string]string, error) 16 | // @TODO: Does ID() work better as retrieving digests? 17 | ID(ctx context.Context, cli *client.Client) (string, error) 18 | Clean(ctx context.Context, cli *client.Client) error 19 | Stop(ctx context.Context, cli *client.Client) error 20 | StopAndRemove(ctx context.Context, cli *client.Client) error 21 | Remove(ctx context.Context, cli *client.Client) error 22 | 23 | SetField(ctx context.Context, cli *client.Client, name string, value interface{}) error 24 | GetFieldString(ctx context.Context, cli *client.Client, field string) (string, error) 25 | GetFieldInt(ctx context.Context, cli *client.Client, field string) (int, error) 26 | GetFieldBool(ctx context.Context, cli *client.Client, field string) (bool, error) 27 | } 28 | -------------------------------------------------------------------------------- /internal/service/docker/dnsmasq/dnsmasq.go: -------------------------------------------------------------------------------- 1 | package dnsmasq 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/docker/docker/api/types/container" 7 | "github.com/docker/docker/api/types/network" 8 | "github.com/docker/go-connections/nat" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker" 11 | ) 12 | 13 | // New will provide the standard object for the dnsmasq container. 14 | func New(c *docker.Params) docker.Service { 15 | return docker.Service{ 16 | Config: container.Config{ 17 | Image: "pygmystack/dnsmasq", 18 | Cmd: []string{ 19 | "--log-facility=-", 20 | "-A", 21 | fmt.Sprintf("/%s/127.0.0.1", c.Domain), 22 | }, 23 | Labels: map[string]string{ 24 | "pygmy.defaults": "true", 25 | "pygmy.enable": "true", 26 | "pygmy.name": "amazeeio-dnsmasq", 27 | "pygmy.weight": "13", 28 | }, 29 | }, 30 | HostConfig: container.HostConfig{ 31 | AutoRemove: false, 32 | CapAdd: []string{"NET_ADMIN"}, 33 | IpcMode: "private", 34 | PortBindings: nat.PortMap{ 35 | "53/tcp": []nat.PortBinding{ 36 | { 37 | HostIP: "", 38 | HostPort: "6053", 39 | }, 40 | }, 41 | "53/udp": []nat.PortBinding{ 42 | { 43 | HostIP: "", 44 | HostPort: "6053", 45 | }, 46 | }, 47 | }, 48 | RestartPolicy: container.RestartPolicy{ 49 | Name: "unless-stopped", 50 | MaximumRetryCount: 0, 51 | }, 52 | }, 53 | NetworkConfig: network.NetworkingConfig{}, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/service/docker/dnsmasq/dnsmasq_test.go: -------------------------------------------------------------------------------- 1 | package dnsmasq_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/docker/docker/api/types/container" 8 | "github.com/docker/go-connections/nat" 9 | . "github.com/smartystreets/goconvey/convey" 10 | 11 | "github.com/pygmystack/pygmy/internal/runtime/docker" 12 | "github.com/pygmystack/pygmy/internal/service/docker/dnsmasq" 13 | ) 14 | 15 | func Example() { 16 | dnsmasq.New(&docker.Params{}) 17 | } 18 | 19 | func Test(t *testing.T) { 20 | Convey("DNSMasq: Field equality tests...", t, func() { 21 | obj := dnsmasq.New(&docker.Params{Domain: "docker.amazee.io"}) 22 | 23 | So(obj.Config.Image, ShouldContainSubstring, "pygmystack/dnsmasq") 24 | So(fmt.Sprint(obj.Config.Cmd), ShouldEqual, fmt.Sprint([]string{"--log-facility=-", "-A", "/docker.amazee.io/127.0.0.1"})) 25 | So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") 26 | So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") 27 | So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-dnsmasq") 28 | So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "13") 29 | So(obj.HostConfig.AutoRemove, ShouldBeFalse) 30 | So(fmt.Sprint(obj.HostConfig.CapAdd), ShouldEqual, fmt.Sprint([]string{"NET_ADMIN"})) 31 | So(obj.HostConfig.IpcMode.IsPrivate(), ShouldBeTrue) 32 | So(fmt.Sprint(obj.HostConfig.PortBindings), ShouldEqual, fmt.Sprint(nat.PortMap{"53/tcp": []nat.PortBinding{{HostIP: "", HostPort: "6053"}}, "53/udp": []nat.PortBinding{{HostIP: "", HostPort: "6053"}}})) 33 | So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, container.RestartPolicyMode("unless-stopped")) 34 | So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldBeZeroValue) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /internal/service/docker/haproxy/haproxy.go: -------------------------------------------------------------------------------- 1 | package haproxy 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/docker/docker/api/types/container" 7 | "github.com/docker/docker/api/types/network" 8 | "github.com/docker/go-connections/nat" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker" 11 | ) 12 | 13 | // New will provide the standard object for the haproxy container. 14 | func New(c *docker.Params) docker.Service { 15 | return docker.Service{ 16 | Config: container.Config{ 17 | Image: "pygmystack/haproxy", 18 | Labels: map[string]string{ 19 | "pygmy.defaults": "true", 20 | "pygmy.enable": "true", 21 | "pygmy.name": "amazeeio-haproxy", 22 | "pygmy.network": "amazeeio-network", 23 | "pygmy.url": fmt.Sprintf("http://%s/stats", c.Domain), 24 | "pygmy.weight": "14", 25 | }, 26 | Env: []string{ 27 | fmt.Sprintf("AMAZEEIO_URL=%s", c.Domain), 28 | }, 29 | }, 30 | HostConfig: container.HostConfig{ 31 | Binds: []string{"/var/run/docker.sock:/tmp/docker.sock"}, 32 | AutoRemove: false, 33 | PortBindings: nil, 34 | RestartPolicy: container.RestartPolicy{ 35 | Name: "unless-stopped", 36 | MaximumRetryCount: 0, 37 | }, 38 | }, 39 | NetworkConfig: network.NetworkingConfig{}, 40 | } 41 | } 42 | 43 | // NewDefaultPorts will provide the standard ports used for merging into the 44 | // haproxy config. 45 | func NewDefaultPorts() docker.Service { 46 | return docker.Service{ 47 | HostConfig: container.HostConfig{ 48 | PortBindings: nat.PortMap{ 49 | "80/tcp": []nat.PortBinding{ 50 | { 51 | HostIP: "", 52 | HostPort: "80", 53 | }, 54 | }, 55 | "443/tcp": []nat.PortBinding{ 56 | { 57 | HostIP: "", 58 | HostPort: "443", 59 | }, 60 | }, 61 | }, 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/service/docker/haproxy/haproxy_test.go: -------------------------------------------------------------------------------- 1 | package haproxy_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/docker/docker/api/types/container" 8 | "github.com/docker/go-connections/nat" 9 | . "github.com/smartystreets/goconvey/convey" 10 | 11 | "github.com/pygmystack/pygmy/internal/runtime/docker" 12 | "github.com/pygmystack/pygmy/internal/service/docker/haproxy" 13 | ) 14 | 15 | func Example() { 16 | haproxy.New(&docker.Params{}) 17 | haproxy.NewDefaultPorts() 18 | } 19 | 20 | func Test(t *testing.T) { 21 | Convey("HAProxy: Field equality tests...", t, func() { 22 | obj := haproxy.New(&docker.Params{Domain: "docker.amazee.io"}) 23 | objPorts := haproxy.NewDefaultPorts() 24 | So(obj.Config.Image, ShouldContainSubstring, "pygmystack/haproxy") 25 | So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") 26 | So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") 27 | So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-haproxy") 28 | So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") 29 | So(obj.Config.Labels["pygmy.url"], ShouldEqual, "http://docker.amazee.io/stats") 30 | So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "14") 31 | So(obj.HostConfig.AutoRemove, ShouldBeFalse) 32 | So(fmt.Sprint(obj.HostConfig.Binds), ShouldEqual, fmt.Sprint([]string{"/var/run/docker.sock:/tmp/docker.sock"})) 33 | So(obj.HostConfig.PortBindings, ShouldEqual, nat.PortMap(nil)) 34 | So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, container.RestartPolicyMode("unless-stopped")) 35 | So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldEqual, 0) 36 | So(fmt.Sprint(objPorts.HostConfig.PortBindings), ShouldEqual, fmt.Sprint(nat.PortMap{"80/tcp": []nat.PortBinding{{HostIP: "", HostPort: "80"}}, "443/tcp": []nat.PortBinding{{HostIP: "", HostPort: "443"}}})) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/service/docker/mailhog/mailhog.go: -------------------------------------------------------------------------------- 1 | package mailhog 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/docker/docker/api/types/container" 7 | "github.com/docker/docker/api/types/network" 8 | "github.com/docker/go-connections/nat" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker" 11 | ) 12 | 13 | // New will provide the standard object for the mailhog container. 14 | func New(c *docker.Params) docker.Service { 15 | return docker.Service{ 16 | Config: container.Config{ 17 | User: "0", 18 | ExposedPorts: nat.PortSet{ 19 | "80/tcp": struct{}{}, 20 | "1025/tcp": struct{}{}, 21 | "8025/tcp": struct{}{}, 22 | }, 23 | Env: []string{ 24 | "MH_UI_BIND_ADDR=0.0.0.0:80", 25 | "MH_API_BIND_ADDR=0.0.0.0:80", 26 | "AMAZEEIO=AMAZEEIO", 27 | fmt.Sprintf("AMAZEEIO_URL=mailhog.%s", c.Domain), 28 | }, 29 | Image: "pygmystack/mailhog", 30 | Labels: map[string]string{ 31 | "pygmy.defaults": "true", 32 | "pygmy.enable": "true", 33 | "pygmy.name": "amazeeio-mailhog", 34 | "pygmy.network": "amazeeio-network", 35 | "pygmy.url": fmt.Sprintf("http://mailhog.%s", c.Domain), 36 | "pygmy.weight": "15", 37 | }, 38 | }, 39 | HostConfig: container.HostConfig{ 40 | AutoRemove: false, 41 | RestartPolicy: container.RestartPolicy{ 42 | Name: "unless-stopped", 43 | MaximumRetryCount: 0, 44 | }, 45 | }, 46 | NetworkConfig: network.NetworkingConfig{}, 47 | } 48 | 49 | } 50 | 51 | // NewDefaultPorts will provide the standard ports used for merging into the 52 | // mailhog config. 53 | func NewDefaultPorts() docker.Service { 54 | return docker.Service{ 55 | HostConfig: container.HostConfig{ 56 | PortBindings: nat.PortMap{ 57 | "1025/tcp": []nat.PortBinding{ 58 | { 59 | HostIP: "", 60 | HostPort: "1025", 61 | }, 62 | }, 63 | }, 64 | }, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/service/docker/mailhog/mailhog_test.go: -------------------------------------------------------------------------------- 1 | package mailhog_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/docker/docker/api/types/container" 8 | "github.com/docker/go-connections/nat" 9 | . "github.com/smartystreets/goconvey/convey" 10 | 11 | "github.com/pygmystack/pygmy/internal/runtime/docker" 12 | "github.com/pygmystack/pygmy/internal/service/docker/mailhog" 13 | ) 14 | 15 | func Example() { 16 | mailhog.New(&docker.Params{}) 17 | mailhog.NewDefaultPorts() 18 | } 19 | 20 | func Test(t *testing.T) { 21 | Convey("MailHog: Field equality tests...", t, func() { 22 | obj := mailhog.New(&docker.Params{Domain: "docker.amazee.io"}) 23 | objPorts := mailhog.NewDefaultPorts() 24 | So(obj.Config.User, ShouldEqual, "0") 25 | So(obj.Config.Image, ShouldContainSubstring, "pygmystack/mailhog") 26 | So(fmt.Sprint(obj.Config.ExposedPorts), ShouldEqual, fmt.Sprint(nat.PortSet{"80/tcp": struct{}{}, "1025/tcp": struct{}{}, "8025/tcp": struct{}{}})) 27 | So(fmt.Sprint(obj.Config.Env), ShouldEqual, fmt.Sprint([]string{"MH_UI_BIND_ADDR=0.0.0.0:80", "MH_API_BIND_ADDR=0.0.0.0:80", "AMAZEEIO=AMAZEEIO", "AMAZEEIO_URL=mailhog.docker.amazee.io"})) 28 | So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") 29 | So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") 30 | So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-mailhog") 31 | So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") 32 | So(obj.Config.Labels["pygmy.url"], ShouldEqual, "http://mailhog.docker.amazee.io") 33 | So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "15") 34 | So(obj.HostConfig.AutoRemove, ShouldBeFalse) 35 | So(obj.HostConfig.PortBindings, ShouldEqual, nat.PortMap(nil)) 36 | So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, container.RestartPolicyMode("unless-stopped")) 37 | So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldEqual, 0) 38 | So(fmt.Sprint(objPorts.HostConfig.PortBindings), ShouldEqual, fmt.Sprint(nat.PortMap{"1025/tcp": []nat.PortBinding{{HostIP: "", HostPort: "1025"}}})) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /internal/service/docker/ssh/agent/ssh_agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/docker/api/types/network" 12 | "github.com/docker/docker/client" 13 | "golang.org/x/crypto/ssh" 14 | 15 | "github.com/pygmystack/pygmy/internal/runtime/docker" 16 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 17 | ) 18 | 19 | // New will provide the standard object for the SSH agent container. 20 | func New() docker.Service { 21 | return docker.Service{ 22 | Config: container.Config{ 23 | Image: "pygmystack/ssh-agent", 24 | Labels: map[string]string{ 25 | "pygmy.defaults": "true", 26 | "pygmy.enable": "true", 27 | "pygmy.name": "amazeeio-ssh-agent", 28 | "pygmy.network": "amazeeio-network", 29 | "pygmy.output": "false", 30 | "pygmy.purpose": "sshagent", 31 | "pygmy.weight": "10", 32 | }, 33 | }, 34 | HostConfig: container.HostConfig{ 35 | AutoRemove: false, 36 | IpcMode: "private", 37 | RestartPolicy: container.RestartPolicy{ 38 | Name: "unless-stopped", 39 | MaximumRetryCount: 0, 40 | }, 41 | }, 42 | NetworkConfig: network.NetworkingConfig{}, 43 | } 44 | } 45 | 46 | // List will grab the output of all running containers with the proper 47 | // config after starting them, and return it. 48 | // which is indicated by the purpose tag. 49 | func List(ctx context.Context, cli *client.Client, service *docker.Service) ([]byte, error) { 50 | name, _ := service.GetFieldString(ctx, cli, "name") 51 | purpose, _ := service.GetFieldString(ctx, cli, "purpose") 52 | if purpose == "showkeys" { 53 | e := service.Start(ctx, cli) 54 | if e != nil { 55 | return []byte{}, e 56 | } 57 | } 58 | return containers.Logs(ctx, cli, name) 59 | } 60 | 61 | // Validate will validate if an SSH key is valid. 62 | func Validate(filePath string) (bool, error) { 63 | 64 | filePath = strings.TrimRight(filePath, ".pub") 65 | content, err := os.ReadFile(filePath) 66 | if err != nil { 67 | fmt.Println("Err") 68 | } 69 | 70 | _, err = ssh.ParsePrivateKey(content) 71 | if err != nil { 72 | return false, err 73 | } 74 | 75 | return true, nil 76 | } 77 | 78 | // Search will determine if an SSH key has been added to the agent. 79 | func Search(ctx context.Context, cli *client.Client, service *docker.Service, key string) (bool, error) { 80 | result := false 81 | if _, err := os.Stat(key); !os.IsNotExist(err) { 82 | stripped := strings.Trim(key, ".pub") 83 | data, err := os.ReadFile(stripped + ".pub") 84 | if err != nil { 85 | return false, err 86 | } 87 | 88 | items, _ := List(ctx, cli, service) 89 | 90 | if len(items) == 0 { 91 | return false, nil 92 | } 93 | 94 | for _, item := range strings.Split(string(items), "\n") { 95 | if strings.Contains(item, "The agent has no identities") { 96 | return false, errors.New(item) 97 | } 98 | if strings.Contains(item, "Error loading key") { 99 | return false, errors.New(item) 100 | } 101 | if strings.Contains(item, string(data)) { 102 | result = true 103 | } 104 | } 105 | } 106 | return result, nil 107 | } 108 | -------------------------------------------------------------------------------- /internal/service/docker/ssh/agent/ssh_agent_test.go: -------------------------------------------------------------------------------- 1 | package agent_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/docker/docker/api/types/container" 7 | "github.com/docker/go-connections/nat" 8 | . "github.com/smartystreets/goconvey/convey" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker" 11 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 12 | "github.com/pygmystack/pygmy/internal/service/docker/ssh/agent" 13 | ) 14 | 15 | //func TestExampleList(t *testing.T) { 16 | // m := &model.Service{} 17 | // c, e := agent.List(*m) 18 | // if c != nil && e != nil { 19 | // t.Fail() 20 | // } 21 | //} 22 | 23 | func TestExampleSearch(t *testing.T) { 24 | cli, ctx, err := internals.NewClient() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | _, err = agent.Search(ctx, cli, &docker.Service{}, "id_rsa.pub") 30 | if err != nil { 31 | t.Fail() 32 | } 33 | } 34 | 35 | func Test(t *testing.T) { 36 | Convey("SSH Agent: Field equality tests...", t, func() { 37 | obj := agent.New() 38 | So(obj.Config.Image, ShouldContainSubstring, "pygmystack/ssh-agent") 39 | So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") 40 | So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") 41 | So(obj.Config.Labels["pygmy.output"], ShouldEqual, "false") 42 | So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-ssh-agent") 43 | So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") 44 | So(obj.Config.Labels["pygmy.purpose"], ShouldEqual, "sshagent") 45 | So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "10") 46 | So(obj.HostConfig.AutoRemove, ShouldBeFalse) 47 | So(obj.HostConfig.IpcMode.IsPrivate(), ShouldBeTrue) 48 | So(obj.HostConfig.PortBindings, ShouldEqual, nat.PortMap(nil)) 49 | So(obj.HostConfig.RestartPolicy.Name, ShouldEqual, container.RestartPolicyMode("unless-stopped")) 50 | So(obj.HostConfig.RestartPolicy.MaximumRetryCount, ShouldEqual, 0) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /internal/service/docker/ssh/key/ssh_addkey.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package key 5 | 6 | import ( 7 | "github.com/docker/docker/api/types/container" 8 | "github.com/docker/docker/api/types/network" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker" 11 | ) 12 | 13 | // NewAdder will provide the standard object for the SSH key adder container. 14 | func NewAdder() docker.Service { 15 | return docker.Service{ 16 | Config: container.Config{ 17 | Image: "pygmystack/ssh-agent", 18 | Labels: map[string]string{ 19 | "pygmy.defaults": "true", 20 | "pygmy.enable": "true", 21 | "pygmy.name": "amazeeio-ssh-agent-add-key", 22 | "pygmy.network": "amazeeio-network", 23 | "pygmy.discrete": "true", 24 | "pygmy.interactive": "true", 25 | "pygmy.output": "false", 26 | "pygmy.purpose": "addkeys", 27 | "pygmy.weight": "31", 28 | }, 29 | Tty: true, 30 | OpenStdin: true, 31 | }, 32 | HostConfig: container.HostConfig{ 33 | AutoRemove: false, 34 | IpcMode: "private", 35 | VolumesFrom: []string{"amazeeio-ssh-agent"}, 36 | }, 37 | NetworkConfig: network.NetworkingConfig{}, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/service/docker/ssh/key/ssh_addkey_test.go: -------------------------------------------------------------------------------- 1 | package key_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | 9 | "github.com/pygmystack/pygmy/internal/service/docker/ssh/key" 10 | ) 11 | 12 | //func ExampleAdd() { 13 | // key.NewAdder() 14 | //} 15 | 16 | func TestAdd(t *testing.T) { 17 | Convey("SSH Key Adder: Field equality tests...", t, func() { 18 | obj := key.NewAdder() 19 | So(obj.Config.Image, ShouldContainSubstring, "pygmystack/ssh-agent") 20 | So(obj.Config.Labels["pygmy.defaults"], ShouldEqual, "true") 21 | So(obj.Config.Labels["pygmy.enable"], ShouldEqual, "true") 22 | So(obj.Config.Labels["pygmy.output"], ShouldEqual, "false") 23 | So(obj.Config.Labels["pygmy.discrete"], ShouldEqual, "true") 24 | So(obj.Config.Labels["pygmy.interactive"], ShouldEqual, "true") 25 | So(obj.Config.Labels["pygmy.name"], ShouldEqual, "amazeeio-ssh-agent-add-key") 26 | So(obj.Config.Labels["pygmy.network"], ShouldEqual, "amazeeio-network") 27 | So(obj.Config.Labels["pygmy.purpose"], ShouldEqual, "addkeys") 28 | So(obj.Config.Labels["pygmy.weight"], ShouldEqual, "31") 29 | So(obj.Config.Tty, ShouldEqual, true) 30 | So(obj.Config.OpenStdin, ShouldEqual, true) 31 | So(obj.HostConfig.AutoRemove, ShouldBeFalse) 32 | So(obj.HostConfig.IpcMode.IsPrivate(), ShouldBeTrue) 33 | So(fmt.Sprint(obj.HostConfig.VolumesFrom), ShouldEqual, fmt.Sprint([]string{"amazeeio-ssh-agent"})) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /internal/service/docker/ssh/key/ssh_addkey_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package key 5 | 6 | import ( 7 | "github.com/docker/docker/api/types/container" 8 | "github.com/docker/docker/api/types/network" 9 | 10 | "github.com/pygmystack/pygmy/internal/runtime/docker" 11 | ) 12 | 13 | // NewAdder will provide the standard object for the SSH key adder container. 14 | func NewAdder() docker.Service { 15 | return docker.Service{ 16 | Config: container.Config{ 17 | Image: "pygmystack/ssh-agent", 18 | Labels: map[string]string{ 19 | "pygmy.defaults": "true", 20 | "pygmy.enable": "true", 21 | "pygmy.name": "amazeeio-ssh-agent-add-key", 22 | "pygmy.network": "amazeeio-network", 23 | "pygmy.discrete": "true", 24 | "pygmy.interactive": "true", 25 | "pygmy.output": "false", 26 | "pygmy.purpose": "addkeys", 27 | "pygmy.weight": "31", 28 | }, 29 | Tty: true, 30 | OpenStdin: true, 31 | }, 32 | HostConfig: container.HostConfig{ 33 | AutoRemove: false, 34 | IpcMode: "private", 35 | VolumesFrom: []string{"amazeeio-ssh-agent"}, 36 | }, 37 | NetworkConfig: network.NetworkingConfig{}, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/utils/color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mattn/go-colorable" 7 | ) 8 | 9 | var colorableOutput = colorable.NewColorableStdout() 10 | 11 | // Print will print text to an interface using a colour via go-colourable. 12 | func Print(input interface{}) { 13 | fmt.Fprint(colorableOutput, input) 14 | } 15 | -------------------------------------------------------------------------------- /internal/utils/endpoint/endpoint.go: -------------------------------------------------------------------------------- 1 | // Package endpoint provides a way to test a HTTP/HTTPS endpoint for a 200 response code. 2 | // note that this package does not support insecure HTTPS at this time. 3 | package endpoint 4 | 5 | import ( 6 | "crypto/tls" 7 | "net/http" 8 | ) 9 | 10 | // Validate will submit a web request to test the container service. 11 | // If a 200 response code is received it will pass and return true. 12 | // Any other result will fail this validation process. 13 | // 14 | // This is to provided to the user through the up and status commands. 15 | func Validate(url string) bool { 16 | 17 | tr := &http.Transport{ 18 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 19 | } 20 | client := &http.Client{Transport: tr} 21 | 22 | // Create a web request 23 | req, err := http.NewRequest("GET", url, nil) 24 | if err != nil { 25 | // Test failed. 26 | return false 27 | } 28 | 29 | // Submit a web request 30 | resp, err := client.Do(req) 31 | if err != nil { 32 | // Test failed. 33 | return false 34 | } 35 | 36 | // Housekeeping 37 | defer resp.Body.Close() 38 | 39 | // The default response for a failed loopback is a 503. 40 | // Because server errors are 503, we should make sure 41 | // we do not get a 5xx response code from the endpoint. 42 | // 500 is known to be a success as well, so start from 501. 43 | 44 | // Check for known failure status response codes (failures): 45 | if resp.StatusCode >= 501 && resp.StatusCode < 600 { 46 | return false 47 | } 48 | 49 | // Test passed. 50 | return true 51 | } 52 | -------------------------------------------------------------------------------- /internal/utils/endpoint/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package endpoint_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | 8 | "github.com/pygmystack/pygmy/internal/utils/endpoint" 9 | ) 10 | 11 | func Example() { 12 | endpoint.Validate("http://127.0.0.1:8080") 13 | } 14 | 15 | func Test(t *testing.T) { 16 | Convey("URL Endpoint tests...", t, func() { 17 | valid := endpoint.Validate("https://www.golang.org/") 18 | So(valid, ShouldBeTrue) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /internal/utils/network/docker/docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | networktypes "github.com/docker/docker/api/types/network" 5 | ) 6 | 7 | // New will generate the defaults for the Docker network. 8 | // If configuration is provided this will not be used at all. 9 | func New() networktypes.Inspect { 10 | return networktypes.Inspect{ 11 | Name: "amazeeio-network", 12 | IPAM: networktypes.IPAM{ 13 | Driver: "", 14 | Options: nil, 15 | Config: []networktypes.IPAMConfig{ 16 | { 17 | Subnet: "10.99.99.0/24", 18 | Gateway: "10.99.99.1", 19 | }, 20 | }, 21 | }, 22 | Labels: map[string]string{ 23 | "pygmy.name": "amazeeio-network", 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/utils/network/docker/docker_test.go: -------------------------------------------------------------------------------- 1 | package docker_test 2 | 3 | import ( 4 | "fmt" 5 | n "github.com/pygmystack/pygmy/internal/utils/network/docker" 6 | "testing" 7 | 8 | "github.com/docker/docker/api/types/network" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func Example() { 13 | n.New() 14 | } 15 | 16 | func Test(t *testing.T) { 17 | Convey("Network: Field equality tests...", t, func() { 18 | obj := n.New() 19 | So(obj.Name, ShouldEqual, "amazeeio-network") 20 | So(obj.IPAM.Driver, ShouldEqual, "") 21 | So(obj.IPAM.Options, ShouldEqual, map[string]string(nil)) 22 | So(fmt.Sprint(obj.IPAM.Config), ShouldEqual, fmt.Sprint([]network.IPAMConfig{{Subnet: "10.99.99.0/24", Gateway: "10.99.99.1"}})) 23 | So(fmt.Sprint(obj.Labels), ShouldEqual, fmt.Sprint(map[string]string{"pygmy.name": "amazeeio-network"})) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/utils/resolv/resolv.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package resolv 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "runtime" 11 | "strings" 12 | 13 | . "github.com/logrusorgru/aurora" 14 | 15 | "github.com/pygmystack/pygmy/internal/runtime/docker" 16 | "github.com/pygmystack/pygmy/internal/utils/color" 17 | ) 18 | 19 | // run will run a shell command and is not exported. 20 | // Shell functionality is exclusive to this package. 21 | func run(args []string) error { 22 | commandArgs := strings.Join(args, " ") 23 | command := exec.Command("sh", "-c", commandArgs) 24 | return command.Run() 25 | } 26 | 27 | // Configure will ensure the given Resolv type a method that can setup a file 28 | // with the contents of Data at File in Folder. This file will route traffic 29 | // on a configured namespace to the localhost and dnsmasq will accept this 30 | // traffic and route it to the docker container. It will remove the file and/or 31 | // rewrite the contents for both MacOS and Linux - Linux will however result in 32 | // removing the string from the file, where MacOS will contain a file with only 33 | // the contents of Data. MacOS will also run the following upon completion of 34 | // this function: 35 | // * sudo ifconfig lo0 alias 172.16.172.16 36 | // * sudo killall mDNSResponder 37 | func (resolv Resolv) Configure(c *docker.Params) { 38 | 39 | var cmdOut []byte 40 | var tmpFile *os.File 41 | 42 | if !resolv.Enabled { 43 | return 44 | } 45 | if resolv.Status(c) { 46 | color.Print(Green(fmt.Sprintf("Already configured resolvr %s\n", resolv.Name))) 47 | } else { 48 | fullPath := fmt.Sprintf("%v%v%v", resolv.Folder, string(os.PathSeparator), resolv.File) 49 | if _, err := os.Stat(fullPath); os.IsNotExist(err) { 50 | // Create the directory if it doesn't exist. 51 | if _, err := os.Stat(resolv.Folder); os.IsNotExist(err) { 52 | if err := run([]string{"sudo", "mkdir", "-p", resolv.Folder}); err != nil { 53 | fmt.Println(err) 54 | } 55 | if err = run([]string{"sudo", "chmod", "777", resolv.Folder}); err != nil { 56 | fmt.Println(err) 57 | } 58 | } 59 | 60 | // Create the file if it doesn't exist. 61 | if _, err := os.Stat(fullPath); os.IsNotExist(err) { 62 | if tmpFile, err = os.CreateTemp("", "pygmy-"); err != nil { 63 | fmt.Println(err) 64 | } 65 | if err = os.Chmod(tmpFile.Name(), 0777); err != nil { 66 | fmt.Println(err) 67 | } 68 | if _, err = tmpFile.WriteString(resolv.Data); err != nil { 69 | fmt.Println(err) 70 | } 71 | if err = run([]string{"sudo", "cp", tmpFile.Name(), fullPath}); err != nil { 72 | fmt.Println(err) 73 | } 74 | } 75 | } else { 76 | 77 | // If the bytes haven't already been written to the file: 78 | if !resolv.statusFileData() { 79 | 80 | if _, err := os.Stat(fullPath); err == nil { 81 | 82 | cmd := exec.Command("/bin/sh", "-c", "cat "+fullPath) 83 | 84 | if cmdOut, err = cmd.Output(); err != nil { 85 | fmt.Println(err.Error()) 86 | fmt.Println("/bin/sh", "-c", "cat "+fullPath) 87 | } 88 | 89 | if tmpFile, err = os.CreateTemp("", "pygmy-"); err != nil { 90 | fmt.Println(err) 91 | } else { 92 | if err = os.Chmod(tmpFile.Name(), 0777); err != nil { 93 | fmt.Println(err) 94 | } 95 | if _, err = tmpFile.WriteString(string(cmdOut)); err != nil { 96 | fmt.Println(err) 97 | } 98 | if _, err = tmpFile.WriteString(resolv.Data); err != nil { 99 | fmt.Println(err) 100 | } 101 | if err = tmpFile.Close(); err != nil { 102 | fmt.Println(err) 103 | } 104 | if err = run([]string{"sudo", "cp", tmpFile.Name(), fullPath}); err != nil { 105 | fmt.Println(err) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | if runtime.GOOS == "darwin" { 113 | ifConfig := exec.Command("/bin/sh", "-c", "sudo ifconfig lo0 alias 172.16.172.16") 114 | if err := ifConfig.Run(); err != nil { 115 | color.Print(Sprintf(Red("error creating loopback UP alias"))) 116 | } 117 | killAll := exec.Command("/bin/sh", "-c", "sudo killall mDNSResponder") 118 | if err := killAll.Run(); err != nil { 119 | color.Print(Sprintf(Red("error restarting mDNSResponder"))) 120 | } 121 | } 122 | 123 | if resolv.Status(c) { 124 | color.Print(Green(fmt.Sprintf("Successfully configured resolvr %s\n", resolv.Name))) 125 | } 126 | } 127 | } 128 | 129 | // Clean will cleanup the resolv file configured to the system and run some 130 | // cleanup commands which were ran at the end of resolv.Configure on MacOS. 131 | func (resolv Resolv) Clean() { 132 | 133 | fullPath := fmt.Sprintf("%v%v%v", resolv.Folder, string(os.PathSeparator), resolv.File) 134 | if runtime.GOOS == "linux" { 135 | if _, err := os.Stat(fullPath); err == nil { 136 | 137 | cmd := exec.Command("/bin/sh", "-c", "cat "+fullPath) 138 | if cmdOut, cmdErr := cmd.Output(); cmdErr != nil { 139 | fmt.Println(cmdErr.Error()) 140 | } else { 141 | if strings.Contains(string(cmdOut), resolv.Data) { 142 | newFile := strings.Replace(string(cmdOut), resolv.Data, "", -1) 143 | if tmpFile, err := os.CreateTemp("", "pygmy-"); err != nil { 144 | fmt.Println(err) 145 | } else { 146 | if err = os.Chmod(tmpFile.Name(), 0777); err != nil { 147 | fmt.Println(err) 148 | } 149 | if _, err = tmpFile.WriteString(newFile); err != nil { 150 | fmt.Println(err) 151 | } 152 | if err = tmpFile.Close(); err != nil { 153 | fmt.Println(err) 154 | } 155 | if err = run([]string{"sudo", "cp", tmpFile.Name(), fullPath}); err != nil { 156 | fmt.Println(err) 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | if _, err := os.Stat(fullPath); err == nil { 165 | err := run([]string{"sudo", "rm", fullPath}) 166 | if err != nil { 167 | fmt.Println(err) 168 | } 169 | if !resolv.statusFile() { 170 | color.Print(Sprintf(Green("Successfully removed resolver file"))) 171 | } 172 | } 173 | 174 | if runtime.GOOS == "darwin" { 175 | 176 | if resolv.statusNet() { 177 | fmt.Println("Removing loopback alias IP (may require sudo)") 178 | ifConfig := exec.Command("/bin/sh", "-c", "sudo ifconfig lo0 -alias 172.16.172.16") 179 | err := ifConfig.Run() 180 | if err != nil { 181 | color.Print(Sprintf(Red("error removing loopback UP alias\n"), Red(err))) 182 | } else { 183 | if !resolv.statusNet() { 184 | color.Print(Sprintf(Green("Successfully removed loopback alias IP.\n"))) 185 | } 186 | } 187 | } 188 | 189 | killAll := exec.Command("/bin/sh", "-c", "sudo killall mDNSResponder\n") 190 | err := killAll.Run() 191 | if err != nil { 192 | color.Print(Sprintf(Red("error restarting mDNSResponder\n"))) 193 | } else { 194 | color.Print(Sprintf(Green("Successfully restarted mDNSResponder\n"))) 195 | } 196 | } 197 | } 198 | 199 | // Status is an exported state function which will check the file contents 200 | // matches Data on Linux, or return the result of three independent checks 201 | // on MacOS including the file, network and data checks. 202 | func (resolv Resolv) Status(c *docker.Params) bool { 203 | 204 | if runtime.GOOS == "darwin" { 205 | return resolv.statusFile() && resolv.statusNet() && resolv.statusFileData() 206 | } 207 | fullPath := fmt.Sprintf("%v%v%v", resolv.Folder, string(os.PathSeparator), resolv.File) 208 | if _, err := os.Stat(fullPath); err == nil { 209 | 210 | cmd := exec.Command("/bin/sh", "-c", "cat "+fullPath) 211 | cmdOut, cmdErr := cmd.Output() 212 | if cmdErr != nil { 213 | fmt.Println(cmdErr.Error()) 214 | } 215 | if strings.Contains(string(cmdOut), resolv.Data) { 216 | return true 217 | } 218 | } 219 | 220 | return false 221 | } 222 | 223 | // statusFileData will check the resolv file contents matches what is expected 224 | func (resolv Resolv) statusFileData() bool { 225 | fullPath := fmt.Sprintf("%v%v%v", resolv.Folder, string(os.PathSeparator), resolv.File) 226 | cmd := exec.Command("/bin/sh", "-c", "cat "+fullPath) 227 | if cmdOut, cmdErr := cmd.Output(); cmdErr != nil { 228 | fmt.Println(cmdErr.Error()) 229 | } else { 230 | return strings.Contains(string(cmdOut), resolv.Data) 231 | } 232 | return false 233 | } 234 | 235 | // statusFile will check the expected file exists 236 | func (resolv Resolv) statusFile() bool { 237 | fullPath := fmt.Sprintf("%v%v%v", resolv.Folder, string(os.PathSeparator), resolv.File) 238 | if _, err := os.Stat(fullPath); !os.IsExist(err) { 239 | return true 240 | } 241 | return false 242 | } 243 | 244 | // statusNet will check the network has the required config. 245 | // One of the original Pygmy's more regular issues was that the network had no 246 | // checks, so the command to make that change was ran as much as logic provided 247 | // and as a result there were some very unusual and unfixable issues. 248 | // This has completely ruled that situation out. 249 | func (resolv Resolv) statusNet() bool { 250 | ifConfigCmd := exec.Command("/bin/sh", "-c", "ifconfig lo0") 251 | out, ifConfigErr := ifConfigCmd.Output() 252 | 253 | if ifConfigErr != nil { 254 | fmt.Println(ifConfigErr.Error()) 255 | return false 256 | } 257 | 258 | return strings.Contains(string(out), "172.16.172.16") 259 | } 260 | -------------------------------------------------------------------------------- /internal/utils/resolv/resolvWin.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package resolv 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "os/exec" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/pygmystack/pygmy/internal/runtime/docker" 14 | ) 15 | 16 | // run will run a shell command and is not exported. 17 | // Shell functionality is exclusive to this package. 18 | func run(args []string) ([]byte, error) { 19 | 20 | powershell, err := exec.LookPath("powershell") 21 | if err != nil { 22 | fmt.Println(err) 23 | } 24 | 25 | // Generate the command, based on input. 26 | cmd := exec.Cmd{} 27 | cmd.Path = powershell 28 | cmd.Args = []string{powershell} 29 | 30 | // Add our arguments to the command. 31 | cmd.Args = append(cmd.Args, args...) 32 | 33 | var output bytes.Buffer 34 | cmd.Stdout = &output 35 | cmd.Stderr = &output 36 | 37 | // Check the errors, return as needed. 38 | var wg sync.WaitGroup 39 | wg.Add(1) 40 | err = cmd.Run() 41 | 42 | if err != nil { 43 | fmt.Println(err) 44 | return []byte{}, err 45 | } 46 | wg.Done() 47 | 48 | return output.Bytes(), nil 49 | 50 | } 51 | 52 | func (resolv Resolv) Clean() { 53 | _, error := run([]string{"Clear-ItemProperty -Path HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters -Name Domain"}) 54 | if error != nil { 55 | fmt.Println(error.Error()) 56 | } 57 | } 58 | func (resolv Resolv) Configure(c *docker.Params) { 59 | if resolv.Enabled { 60 | _, error := run([]string{fmt.Sprintf("Set-ItemProperty -Path HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters -Name Domain -Value %s", c.Domain)}) 61 | if error != nil { 62 | fmt.Println(error.Error()) 63 | } 64 | } 65 | } 66 | 67 | func (resolv Resolv) Status(c *docker.Params) bool { 68 | data, error := run([]string{"Get-ItemProperty -Path HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"}) 69 | if error != nil { 70 | return false 71 | } 72 | for _, v := range strings.Split(string(data), "\n") { 73 | if strings.HasPrefix(v, "Domain") && strings.Contains(v, c.Domain) { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | -------------------------------------------------------------------------------- /internal/utils/resolv/types.go: -------------------------------------------------------------------------------- 1 | package resolv 2 | 3 | // Resolv is a struct of properties which are translates to a local resolv for 4 | // dnsmasq to redirect a given domain suffix to the local docker daemon. 5 | // Windows has a custom solution, however this will be used on both Mac 6 | // and Linux. 7 | type Resolv struct { 8 | Data string `yaml:"contents"` 9 | Enabled bool `yaml:"enable"` 10 | File string `yaml:"file"` 11 | Folder string `yaml:"folder"` 12 | Name string `yaml:"name"` 13 | } 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Amazee.io 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "github.com/pygmystack/pygmy/cmd" 25 | ) 26 | 27 | func main() { 28 | cmd.Execute() 29 | } 30 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/docker/api/types/network" 12 | . "github.com/smartystreets/goconvey/convey" 13 | 14 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/containers" 15 | "github.com/pygmystack/pygmy/internal/runtime/docker/internals/images" 16 | ) 17 | 18 | const ( 19 | dindContainerName = "exampleTestContainer" 20 | binaryReference = "pygmy-linux-amd64" 21 | ) 22 | 23 | var ( 24 | dindID string 25 | ) 26 | 27 | type config struct { 28 | name string 29 | configpath string 30 | endpoints []string 31 | images []string 32 | services []string 33 | servicewithports []string 34 | skipendpointchecks bool 35 | } 36 | 37 | // setup is a configurable pipeline which allows different configurations to 38 | // run to keep the consistency for as many tests as are required. 39 | func setup(t *testing.T, config *config) { 40 | 41 | cli, ctx, err := internals.NewClient() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | var cleanCmd = fmt.Sprintf("/builds/%v clean", binaryReference) 47 | var statusCmd = fmt.Sprintf("/builds/%v status", binaryReference) 48 | var upCmd = fmt.Sprintf("/builds/%v up", binaryReference) 49 | var downCmd = fmt.Sprintf("/builds/%v down", binaryReference) 50 | 51 | if config.configpath != "" { 52 | cleanCmd = fmt.Sprintf("/builds/%v clean --config %v", binaryReference, config.configpath) 53 | statusCmd = fmt.Sprintf("/builds/%v status --config %v", binaryReference, config.configpath) 54 | upCmd = fmt.Sprintf("/builds/%v up --config %v", binaryReference, config.configpath) 55 | downCmd = fmt.Sprintf("/builds/%v dow --config %v", binaryReference, config.configpath) 56 | } 57 | 58 | Convey("Pygmy Application Test: "+config.name, t, func() { 59 | 60 | Convey("Provision environment", func() { 61 | Convey("Image pulled", func() { 62 | _, e := images.Pull(ctx, cli, "library/docker:dind") 63 | So(e, ShouldBeNil) 64 | }) 65 | 66 | Convey("Container created", func() { 67 | currentWorkingDirectory, err := os.Getwd() 68 | So(err, ShouldBeNil) 69 | x, _ := containers.Create(ctx, cli, dindContainerName, container.Config{ 70 | Image: "docker:dind", 71 | }, container.HostConfig{ 72 | AutoRemove: false, 73 | Binds: []string{ 74 | fmt.Sprintf("%v%vbuilds%v:/builds", currentWorkingDirectory, string(os.PathSeparator), string(os.PathSeparator)), 75 | fmt.Sprintf("%v%vexamples%v:/examples", currentWorkingDirectory, string(os.PathSeparator), string(os.PathSeparator)), 76 | }, 77 | Privileged: true, 78 | }, network.NetworkingConfig{}) 79 | 80 | dindID = x.ID 81 | So(dindID, ShouldNotEqual, "") 82 | }) 83 | 84 | Convey("Container started", func() { 85 | err := containers.Start(ctx, cli, dindContainerName, container.StartOptions{}) 86 | So(err, ShouldEqual, nil) 87 | }) 88 | }) 89 | 90 | Convey("Populating Daemon", func() { 91 | 92 | Convey("Container has started the daemon", func() { 93 | _, e := containers.Exec(ctx, cli, dindContainerName, "dockerd") 94 | So(e, ShouldEqual, nil) 95 | time.Sleep(time.Second * 2) 96 | }) 97 | 98 | e := containers.Start(ctx, cli, dindContainerName, container.StartOptions{}) 99 | if e != nil { 100 | fmt.Println(e) 101 | } 102 | 103 | for _, image := range config.images { 104 | Convey("Pulling "+image, func() { 105 | _, e := containers.Exec(ctx, cli, dindContainerName, "docker pull "+image) 106 | time.Sleep(time.Second * 2) 107 | So(e, ShouldBeNil) 108 | }) 109 | } 110 | }) 111 | 112 | Convey("Application Tests", func() { 113 | 114 | Convey("Container has configuration file ("+config.configpath+")", func() { 115 | d, _ := containers.Exec(ctx, cli, dindContainerName, "stat "+config.configpath) 116 | if config.configpath == "" { 117 | SkipSo(string(d), ShouldContainSubstring, config.configpath) 118 | } else { 119 | So(string(d), ShouldContainSubstring, config.configpath) 120 | } 121 | }) 122 | 123 | Convey("Container has compiled binary from host", func() { 124 | d, _ := containers.Exec(ctx, cli, dindContainerName, fmt.Sprintf("stat /builds/%v", binaryReference)) 125 | So(string(d), ShouldContainSubstring, fmt.Sprintf("/builds/%v", binaryReference)) 126 | }) 127 | 128 | d, _ := containers.Exec(ctx, cli, dindContainerName, fmt.Sprintf("/builds/%v", binaryReference)) 129 | Convey("Container can run pygmy", func() { 130 | So(string(d), ShouldContainSubstring, "local containers for local development") 131 | }) 132 | 133 | // While it's safe, we should clean the environment. 134 | _, e := containers.Exec(ctx, cli, dindContainerName, cleanCmd) 135 | if e != nil { 136 | fmt.Println(e) 137 | } 138 | 139 | Convey("Default ports are not allocated", func() { 140 | g, _ := containers.Exec(ctx, cli, dindContainerName, statusCmd) 141 | for _, service := range config.servicewithports { 142 | So(string(g), ShouldContainSubstring, service+" is able to start") 143 | } 144 | }) 145 | 146 | Convey("Pygmy started", func() { 147 | d, _ = containers.Exec(ctx, cli, dindContainerName, upCmd) 148 | if config.configpath != "" { 149 | So(string(d), ShouldContainSubstring, "Using config file: "+config.configpath) 150 | } 151 | for _, service := range config.services { 152 | So(string(d), ShouldContainSubstring, "Successfully started "+service) 153 | } 154 | }) 155 | 156 | Convey("Endpoints are serving", func() { 157 | d, _ = containers.Exec(ctx, cli, dindContainerName, statusCmd) 158 | for _, endpoint := range config.endpoints { 159 | if config.skipendpointchecks { 160 | SkipSo(string(d), ShouldNotContainSubstring, "! "+endpoint) 161 | } else { 162 | So(string(d), ShouldNotContainSubstring, "! "+endpoint) 163 | } 164 | } 165 | }) 166 | }) 167 | 168 | Convey("Environment Cleanup", func() { 169 | Convey("Pygmy has cleaned the environment", func() { 170 | 171 | _, e := containers.Exec(ctx, cli, dindContainerName, downCmd) 172 | So(e, ShouldBeNil) 173 | _, e = containers.Exec(ctx, cli, dindContainerName, cleanCmd) 174 | So(e, ShouldBeNil) 175 | d, _ := containers.Exec(ctx, cli, dindContainerName, statusCmd) 176 | for _, service := range config.services { 177 | So(string(d), ShouldContainSubstring, service+" is not running") 178 | } 179 | So(e, ShouldBeNil) 180 | }) 181 | // System prune container... 182 | Convey("Removing DinD Container", func() { 183 | err := containers.Kill(ctx, cli, "exampleTestContainer") 184 | So(err, ShouldBeNil) 185 | err = containers.Remove(ctx, cli, "exampleTestContainer") 186 | So(err, ShouldBeNil) 187 | }) 188 | }) 189 | }) 190 | } 191 | 192 | // TestDefault will test an environment with no additional configuration. 193 | func TestDefault(t *testing.T) { 194 | configuration := &config{ 195 | name: "default", 196 | configpath: "/examples/pygmy.basic.yml", 197 | endpoints: []string{"http://docker.amazee.io/stats", "http://mailhog.docker.amazee.io"}, 198 | images: []string{"pygmystack/haproxy", "pygmystack/dnsmasq", "pygmystack/mailhog"}, 199 | services: []string{"amazeeio-haproxy", "amazeeio-dnsmasq", "amazeeio-mailhog"}, 200 | servicewithports: []string{"amazeeio-haproxy", "amazeeio-mailhog"}, 201 | skipendpointchecks: false, 202 | } 203 | setup(t, configuration) 204 | } 205 | 206 | // TestCustom will test a highly customised environment. 207 | func TestCustom(t *testing.T) { 208 | configuration := &config{ 209 | name: "custom", 210 | configpath: "/examples/pygmy.complex.yml", 211 | endpoints: []string{"http://traefik.docker.amazee.io", "http://mailhog.docker.amazee.io", "http://phpmyadmin.docker.amazee.io"}, 212 | images: []string{"pygmystack/ssh-agent", "pygmystack/mailhog", "phpmyadmin/phpmyadmin", "library/traefik:v2.1.3"}, 213 | services: []string{"unofficial-traefik-2", "unofficial-phpmyadmin", "amazeeio-mailhog"}, 214 | servicewithports: []string{"amazeeio-mailhog", "unofficial-phpmyadmin", "unofficial-traefik-2"}, 215 | skipendpointchecks: false, 216 | } 217 | setup(t, configuration) 218 | } 219 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pygmy Documentation 2 | theme: readthedocs 3 | 4 | nav: 5 | - Pygmy: index.md 6 | - Installation: installation.md 7 | - Usage: usage.md 8 | - Update: update.md 9 | - Troubleshooting: troubleshooting.md 10 | - SSH Agent: ssh_agent.md 11 | - Local Docker Development: local_docker_development.md 12 | - Drupal Site Containers: drupal_site_containers.md 13 | - Connecting to MySQL externally: connect_to_mysql_from_external.md 14 | - Mapping additional ports: map_addtitional_ports.md 15 | - Customisation: 16 | - Introduction: customisation/introduction.md 17 | 18 | markdown_extensions: 19 | - toc: 20 | permalink: true 21 | baselevel: 1 22 | - admonition 23 | --------------------------------------------------------------------------------