├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── SUPPORT.md ├── .gitignore ├── .go-version ├── .travis.yml ├── CHANGELOG.md ├── GNUmakefile ├── LICENSE ├── README.md ├── docker ├── config.go ├── data_source_docker_network.go ├── data_source_docker_network_test.go ├── data_source_docker_registry_image.go ├── data_source_docker_registry_image_test.go ├── label.go ├── label_migration.go ├── label_migration_test.go ├── provider.go ├── provider_test.go ├── resource_docker_config.go ├── resource_docker_config_test.go ├── resource_docker_container.go ├── resource_docker_container_funcs.go ├── resource_docker_container_migrate.go ├── resource_docker_container_test.go ├── resource_docker_container_v1.go ├── resource_docker_image.go ├── resource_docker_image_funcs.go ├── resource_docker_image_test.go ├── resource_docker_network.go ├── resource_docker_network_funcs.go ├── resource_docker_network_test.go ├── resource_docker_registry_image.go ├── resource_docker_registry_image_funcs.go ├── resource_docker_registry_image_funcs_test.go ├── resource_docker_secret.go ├── resource_docker_secret_test.go ├── resource_docker_service.go ├── resource_docker_service_funcs.go ├── resource_docker_service_test.go ├── resource_docker_volume.go ├── resource_docker_volume_test.go ├── structures_service.go ├── validators.go └── validators_test.go ├── examples └── ssh-protocol │ ├── README.md │ └── main.tf ├── go.mod ├── go.sum ├── main.go ├── scripts ├── changelog-links.sh ├── compile.sh ├── errcheck.sh ├── gofmtcheck.sh ├── gogetcookie.sh ├── runAccTests.bat ├── testacc_cleanup.sh ├── testacc_full.sh ├── testacc_setup.sh └── testing │ ├── Dockerfile │ ├── configs.json │ ├── docker_registry_image_context │ ├── Dockerfile │ └── empty │ ├── dockerconfig.json │ ├── secrets.json │ ├── server_v1.js │ ├── server_v2.js │ ├── server_v3.js │ └── setup_private_registry.bat └── website ├── docker.erb └── docs ├── d ├── docker_network.html.markdown └── registry_image.html.markdown ├── index.html.markdown └── r ├── config.html.markdown ├── container.html.markdown ├── image.html.markdown ├── network.html.markdown ├── registry_image.html.markdown ├── secret.html.markdown ├── service.html.markdown └── volume.html.markdown /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hi there, 2 | 3 | Thank you for opening an issue. Please note that we try to keep the Terraform issue tracker reserved for bug reports and feature requests. For general usage questions, please see: https://www.terraform.io/community.html. 4 | 5 | ### Terraform Version 6 | Run `terraform -v` to show the version. If you are not running the latest version of Terraform, please upgrade because your issue may have already been fixed. 7 | 8 | ### Affected Resource(s) 9 | Please list the resources as a list, for example: 10 | - opc_instance 11 | - opc_storage_volume 12 | 13 | If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this. 14 | 15 | ### Terraform Configuration Files 16 | ```hcl 17 | # Copy-paste your Terraform configurations here - for large Terraform configs, 18 | # please use a service like Dropbox and share a link to the ZIP file. For 19 | # security, you can also encrypt the files using our GPG public key. 20 | ``` 21 | 22 | ### Debug Output 23 | Please provider a link to a GitHub Gist containing the complete debug output: https://www.terraform.io/docs/internals/debugging.html. Please do NOT paste the debug output in the issue; just paste a link to the Gist. 24 | 25 | ### Panic Output 26 | If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. 27 | 28 | ### Expected Behavior 29 | What should have happened? 30 | 31 | ### Actual Behavior 32 | What actually happened? 33 | 34 | ### Steps to Reproduce 35 | Please list the steps required to reproduce the issue, for example: 36 | 1. `terraform apply` 37 | 38 | ### Important Factoids 39 | Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs? 40 | 41 | ### References 42 | Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example: 43 | - GH-1234 44 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Terraform is a mature project with a growing community. There are active, dedicated people willing to help you through various mediums. 4 | 5 | Take a look at those mediums listed at https://www.terraform.io/community.html 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | .DS_Store 4 | example.tf 5 | terraform.tfplan 6 | terraform.tfstate 7 | bin/ 8 | modules-dev/ 9 | /pkg/ 10 | website/.vagrant 11 | website/.bundle 12 | website/build 13 | website/node_modules 14 | .vagrant/ 15 | *.backup 16 | ./*.tfstate 17 | .terraform/ 18 | *.log 19 | *.bak 20 | *~ 21 | .*.swp 22 | .idea 23 | *.iml 24 | *.test 25 | *.iml 26 | .vscode 27 | 28 | website/vendor 29 | 30 | # Test exclusions 31 | !command/test-fixtures/**/*.tfstate 32 | !command/test-fixtures/**/.terraform/ 33 | scripts/testing/auth 34 | scripts/testing/certs 35 | 36 | # build outputs 37 | results 38 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.15.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | install: 4 | # This script is used by the Travis build to install a cookie for 5 | # go.googlesource.com so rate limits are higher when using `go get` to fetch 6 | # packages that live there. 7 | # See: https://github.com/golang/go/issues/12933 8 | - bash scripts/gogetcookie.sh 9 | 10 | env: 11 | - GOPROXY=https://gocenter.io,https://proxy.golang.org,direct 12 | 13 | branches: 14 | only: 15 | - master 16 | 17 | matrix: 18 | fast_finish: true 19 | 20 | allow_failures: 21 | - go: tip 22 | - os: osx 23 | - os: windows 24 | 25 | include: 26 | #################################### 27 | # Acceptance tests 28 | #################################### 29 | - os: linux 30 | name: "Acceptance tests" 31 | dist: bionic 32 | go: "1.15.x" 33 | services: 34 | - docker 35 | sudo: required 36 | before_install: 37 | # locally: docker run -it ubuntu:bionic bash (https://ubuntu.pkgs.org/18.04/docker-ce-stable-amd64/) 38 | - sudo apt-get update 39 | - sudo apt-get -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common 40 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 41 | - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 42 | - sudo apt-get update 43 | # apt-cache policy docker-ce 44 | - sudo apt-get -y install docker-ce=5:19.03.5~3-0~ubuntu-bionic 45 | - docker version 46 | # Allow local registry to be insecure 47 | - sudo sed 's/DOCKER_OPTS="/DOCKER_OPTS="--insecure-registry=127.0.0.1:15000 /g' -i /etc/default/docker 48 | - sudo cat /etc/default/docker 49 | - sudo service docker restart 50 | script: 51 | - make testacc 52 | 53 | # https://golang.org/doc/devel/release.html#policy 54 | - os: linux 55 | name: "Build (golang current amd64)" 56 | dist: bionic 57 | go: "1.15.x" 58 | script: 59 | - make compile 60 | 61 | - os: linux 62 | name: "Build (golang previous amd64)" 63 | dist: bionic 64 | go: "1.14.x" 65 | script: 66 | - make compile 67 | 68 | #################################### 69 | # Unit, vet, website, etc 70 | #################################### 71 | - os: linux 72 | name: "Unit-tests, vet, and website" 73 | dist: bionic 74 | go: "1.15.x" 75 | script: 76 | - make vet 77 | - make test 78 | - make website-test 79 | 80 | #################################### 81 | # Windows and Mac 82 | #################################### 83 | - os: osx 84 | name: "Build (golang current)" 85 | go: "1.15.x" 86 | script: 87 | - XC_OS=darwin make compile 88 | 89 | # XXX it doesn't seem possible right now to run linux containers on windows 90 | # see: https://travis-ci.community/t/docker-linux-containers-on-windows/301 91 | # --platform does not work, apparently missing experimental features being enabled for dockerd in the host 92 | - os: windows 93 | name: "Build (golang current)" 94 | # name: "Acceptance tests" 95 | go: "1.15.x" 96 | # services: 97 | # - docker 98 | script: 99 | - go install 100 | # - scripts/runAccTests.bat 101 | 102 | #################################### 103 | # Go tip 104 | #################################### 105 | - os: linux 106 | name: "Build (golang future amd64)" 107 | dist: bionic 108 | go: tip 109 | script: 110 | - make compile -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.8.0 (Unreleased) 2 | ## 2.7.2 (August 03, 2020) 3 | 4 | BUG FIXES 5 | * Fix port objects with the same internal port but different protocol trigger recreation of container ([#274](https://github.com/terraform-providers/ 6 | terraform-provider-docker/pull/274)) 7 | * Fix panic to migrate schema of docker_container from v1 to v2 ([#271](https://github.com/terraform-providers/ 8 | terraform-provider-docker/pull/271)) 9 | * Set `Computed: true` and separate files of resourceDockerContainerV1 ([#272](https://github.com/terraform-providers/terraform-provider-docker/pull/272)) 10 | * Prevent force recreate of container about some attributes ([#269](https://github.com/terraform-providers/terraform-provider-docker/pull/269)) 11 | 12 | DOCS: 13 | * Typo in container.html.markdown ([#278](https://github.com/terraform-providers/terraform-provider-docker/pull/278))( 14 | * Update service.html.markdown ([#281](https://github.com/terraform-providers/terraform-provider-docker/pull/281)) 15 | 16 | ## 2.7.1 (June 05, 2020) 17 | 18 | BUG FIXES 19 | * prevent force recreate of container about some attributes ([#269](https://github.com/terraform-providers/terraform-provider-docker/issues/269)) 20 | 21 | ## 2.7.0 (February 10, 2020) 22 | 23 | IMPROVEMENTS: 24 | * support to import some docker_container's attributes ([#234](https://github.com/terraform-providers/terraform-provider-docker/issues/234)) 25 | * make UID, GID, & mode for Docker secrets and configs configurable ([#231](https://github.com/terraform-providers/ 26 | terraform-provider-docker/pull/231)) 27 | 28 | BUG FIXES: 29 | * Allow use of `source` file instead of content / content_base64 ([#240](https://github.com/terraform-providers/ 30 | terraform-provider-docker/pull/240)) 31 | * Correct IPAM config read on the data provider ([#229](https://github.com/terraform-providers/ 32 | terraform-provider-docker/pull/229)) 33 | * `published_port` is not correctly populated on docker_service resource ([#222](https://github.com/terraform-providers/terraform-provider-docker/issues/222)) 34 | * Registry Config File MUST be a file reference ([#224](https://github.com/terraform-providers/terraform-provider-docker/issues/224)) 35 | * Allow zero replicas ([#220](https://github.com/terraform-providers/ 36 | terraform-provider-docker/pull/220)) 37 | * fixing the label schema for HCL2 ([#217](https://github.com/terraform-providers/ 38 | terraform-provider-docker/pull/217)) 39 | 40 | DOCS: 41 | * Update documentation to reflect changes in TF v12 ([#228](https://github.com/terraform-providers/ 42 | terraform-provider-docker/pull/228)) 43 | 44 | CI: 45 | * bumps docker `19.03` and ubuntu `bionic` ([#241](https://github.com/terraform-providers/ 46 | terraform-provider-docker/pull/241)) 47 | 48 | ## 2.6.0 (November 25, 2019) 49 | 50 | IMPROVEMENTS: 51 | * adds import for resources ([#99](https://github.com/terraform-providers/terraform-provider-docker/issues/99)) 52 | * supports --read-only root fs ([#203](https://github.com/terraform-providers/terraform-provider-docker/issues/203)) 53 | 54 | DOCS 55 | * corrects mounts block name in docs ([#218](https://github.com/terraform-providers/terraform-provider-docker/pull/218)) 56 | 57 | 58 | ## 2.5.0 (October 15, 2019) 59 | 60 | IMPROVEMENTS: 61 | * ci: update to go 1.13 ([#198](https://github.com/terraform-providers/terraform-provider-docker/issues/198)) 62 | * feat: migrate to standalone plugin sdk ([#197](https://github.com/terraform-providers/terraform-provider-docker/issues/197)) 63 | 64 | BUG FIXES: 65 | * fix: removes whitelists of attributes ([#208](https://github.com/terraform-providers/terraform-provider-docker/issues/208)) 66 | * fix: splunk Log Driver missing from container `log_driver` ([#204](https://github.com/terraform-providers/terraform-provider-docker/issues/204)) 67 | 68 | 69 | ## 2.4.0 (October 07, 2019) 70 | 71 | IMPROVEMENTS: 72 | * feat: adds `shm_size attribute` for `docker_container` resource ([#164](https://github.com/terraform-providers/terraform-provider-docker/issues/164)) 73 | * feat: supports for group-add ([#191](https://github.com/terraform-providers/terraform-provider-docker/issues/191)) 74 | 75 | BUG FIXES: 76 | * fix: binary upload as base 64 content ([#48](https://github.com/terraform-providers/terraform-provider-docker/issues/48)) 77 | * fix: service env truncation for multiple delimiters ([#121](https://github.com/terraform-providers/terraform-provider-docker/issues/121)) 78 | * fix: allows docker_registry_image to read from AWS ECR registry ([#186](https://github.com/terraform-providers/terraform-provider-docker/issues/186)) 79 | 80 | DOCS 81 | * Removes duplicate `start_period` entry in `healthcheck` section of the documentation for `docker_service` ([#189](https://github.com/terraform-providers/terraform-provider-docker/pull/189)) 82 | 83 | ## 2.3.0 (September 23, 2019) 84 | 85 | IMPROVEMENTS: 86 | * feat: adds container ipc mode ([#12](https://github.com/terraform-providers/terraform-provider-docker/issues/12)) 87 | * feat: adds container working dir ([#146](https://github.com/terraform-providers/terraform-provider-docker/issues/146)) 88 | * remove usage of config pkg ([#183](https://github.com/terraform-providers/terraform-provider-docker/pull/183)) 89 | 90 | BUG FIXES: 91 | * fix for destroy_grace_seconds is not adhered ([#174](https://github.com/terraform-providers/terraform-provider-docker/issues/174)) 92 | 93 | ## 2.2.0 (August 22, 2019) 94 | 95 | IMPROVEMENTS 96 | * Docker client negotiates the version with the server instead of using a fixed version ([#173](https://github.com/terraform-providers/terraform-provider-docker/issues/173)) 97 | 98 | DOCS 99 | * Fixes section links so they point to the right id ([#176](https://github.com/terraform-providers/terraform-provider-docker/issues/176)) 100 | 101 | ## 2.1.1 (August 08, 2019) 102 | 103 | BUG FIXES 104 | * Fixes 'No changes' for containers when all port blocks have been removed ([#167](https://github.com/terraform-providers/terraform-provider-docker/issues/167)) 105 | 106 | ## 2.1.0 (July 19, 2019) 107 | 108 | IMPROVEMENTS 109 | * Adds cross-platform support for generic Docker credential helper ([#159](https://github.com/terraform-providers/terraform-provider-docker/pull/159)) 110 | 111 | DOC 112 | * Updates the docs for ssh protocol and mounts ([#158](https://github.com/terraform-providers/terraform-provider-docker/issues/158)) 113 | * Fixes website typo / containers / mount vs mounts ([#162](https://github.com/terraform-providers/terraform-provider-docker/pull/162)) 114 | 115 | ## 2.0.0 (June 25, 2019) 116 | 117 | BREAKING CHANGES 118 | * Updates to Terraform `v0.12` [[#144](https://github.com/terraform-providers/terraform-provider-docker/issues/144)] and ([#150](https://github.com/terraform-providers/terraform-provider-docker/pull/150)) 119 | 120 | IMPROVEMENTS 121 | * Refactors test setup ([#156](https://github.com/terraform-providers/terraform-provider-docker/pull/156)) 122 | * Fixes flaky acceptance tests ([#154](https://github.com/terraform-providers/terraform-provider-docker/pull/154)) 123 | 124 | ## 1.2.0 (May 29, 2019) 125 | 126 | IMPROVEMENTS 127 | * Updates to docker `18.09` and API Version `1.39` ([#114](https://github.com/terraform-providers/terraform-provider-docker/issues/114)) 128 | * Upgrades to go `1.11` ([#116](https://github.com/terraform-providers/terraform-provider-docker/pull/116)) 129 | * Switches to `go modules` ([#124](https://github.com/terraform-providers/terraform-provider-docker/issues/124)) 130 | * Adds data source for networks ([#84](https://github.com/terraform-providers/terraform-provider-docker/issues/84)) 131 | * Adds `ssh` protocol support ([#153](https://github.com/terraform-providers/terraform-provider-docker/issues/153)) 132 | * Adds docker container mounts support ([#147](https://github.com/terraform-providers/terraform-provider-docker/pull/147)) 133 | 134 | BUG FIXES 135 | * Fixes image pulling and local registry connections ([#143](https://github.com/terraform-providers/terraform-provider-docker/pull/143)) 136 | 137 | ## 1.1.1 (March 08, 2019) 138 | 139 | BUG FIXES 140 | * Fixes no more 'force new resource' for container ports when 141 | there are no changes. This was caused to the ascending order. See ([#110](https://github.com/terraform-providers/terraform-provider-docker/issues/110)) 142 | for details and ([#115](https://github.com/terraform-providers/terraform-provider-docker/pull/115)) 143 | * Normalize blank port IP's to 0.0.0.0 ([#128](https://github.com/terraform-providers/terraform-provider-docker/pull/128)) 144 | 145 | BUILD 146 | * Simplify Dockerfile(s) for tests ([#135](https://github.com/terraform-providers/terraform-provider-docker/pull/135)) 147 | * Skip test if swap limit isn't available ([#136](https://github.com/terraform-providers/terraform-provider-docker/pull/136)) 148 | 149 | DOCS 150 | * Corrects `networks_advanced` section ([#109](https://github.com/terraform-providers/terraform-provider-docker/issues/109)) 151 | * Corrects `tmpfs_options` section ([#122](https://github.com/terraform-providers/terraform-provider-docker/issues/122)) 152 | * Corrects indentation for container in docs ([#126](https://github.com/terraform-providers/terraform-provider-docker/issues/126)) 153 | * Fix syntax error in docker_service example and make all examples adhere to terraform fmt ([#137](https://github.com/terraform-providers/terraform-provider-docker/pull/137)) 154 | 155 | ## 1.1.0 (October 30, 2018) 156 | 157 | IMPROVEMENTS 158 | * Adds labels for `network`, `volume` and `secret` to support docker stacks. ([#92](https://github.com/terraform-providers/terraform-provider-docker/pull/92)) 159 | * Adds `rm` and `attach` options to execute short-lived containers ([#43](https://github.com/terraform-providers/terraform-provider-docker/issues/43)] and [[#106](https://github.com/terraform-providers/terraform-provider-docker/pull/106)) 160 | * Adds container healthcheck([#93](https://github.com/terraform-providers/terraform-provider-docker/pull/93)) 161 | * Adds the docker container start flag ([#62](https://github.com/terraform-providers/terraform-provider-docker/issues/62)] and [[#94](https://github.com/terraform-providers/terraform-provider-docker/pull/94)) 162 | * Adds `cpu_set` to docker container ([#41](https://github.com/terraform-providers/terraform-provider-docker/pull/41)) 163 | * Simplifies the image options parser and adds missing registry combinations ([#49](https://github.com/terraform-providers/terraform-provider-docker/pull/49)) 164 | * Adds container static IPv4/IPv6 address. Marks network and network_alias as deprecated. ([#105](https://github.com/terraform-providers/terraform-provider-docker/pull/105)) 165 | * Adds container logs option ([#108](https://github.com/terraform-providers/terraform-provider-docker/pull/108)) 166 | 167 | BUG FIXES 168 | * Fixes that new network were appended to the default bridge ([#10](https://github.com/terraform-providers/terraform-provider-docker/issues/10)) 169 | * Fixes that container resource returns a non-existent IP address ([#36](https://github.com/terraform-providers/terraform-provider-docker/issues/36)) 170 | * Fixes container's ip_address is empty when using custom network ([#9](https://github.com/terraform-providers/terraform-provider-docker/issues/9)] and [[#50](https://github.com/terraform-providers/terraform-provider-docker/pull/50)) 171 | * Fixes terraform destroy failing to remove a bridge network ([#98](https://github.com/terraform-providers/terraform-provider-docker/issues/98)] and [[#50](https://github.com/terraform-providers/terraform-provider-docker/pull/50)) 172 | 173 | 174 | ## 1.0.4 (October 17, 2018) 175 | 176 | BUG FIXES 177 | * Support and fix for random external ports for containers [[#102](https://github.com/terraform-providers/terraform-provider-docker/issues/102)] and ([#103](https://github.com/terraform-providers/terraform-provider-docker/pull/103)) 178 | 179 | ## 1.0.3 (October 12, 2018) 180 | 181 | IMPROVEMENTS 182 | * Add support for running tests on Windows [[#54](https://github.com/terraform-providers/terraform-provider-docker/issues/54)] and ([#90](https://github.com/terraform-providers/terraform-provider-docker/pull/90)) 183 | * Add options for PID and user namespace mode [[#88](https://github.com/terraform-providers/terraform-provider-docker/issues/88)] and ([#96](https://github.com/terraform-providers/terraform-provider-docker/pull/96)) 184 | 185 | BUG FIXES 186 | * Fixes issue with internal and external ports on containers [[#8](https://github.com/terraform-providers/terraform-provider-docker/issues/8)] and ([#89](https://github.com/terraform-providers/terraform-provider-docker/pull/89)) 187 | * Fixes `tfstate` having correct external port for containers [[#73](https://github.com/terraform-providers/terraform-provider-docker/issues/73)] and ([#95](https://github.com/terraform-providers/terraform-provider-docker/pull/95)) 188 | * Fixes that a `docker_image` can be pulled with its SHA256 tag/repo digest [[#79](https://github.com/terraform-providers/terraform-provider-docker/issues/79)] and ([#97](https://github.com/terraform-providers/terraform-provider-docker/pull/97)) 189 | 190 | ## 1.0.2 (September 27, 2018) 191 | 192 | BUG FIXES 193 | * Fixes connection via TLS to docker host with file contents ([#86](https://github.com/terraform-providers/terraform-provider-docker/issues/86)) 194 | * Skips TLS verification if `ca_material` is not set ([#14](https://github.com/terraform-providers/terraform-provider-docker/issues/14)) 195 | 196 | ## 1.0.1 (August 06, 2018) 197 | 198 | BUG FIXES 199 | * Fixes empty strings on mapping from map to slice causes ([#81](https://github.com/terraform-providers/terraform-provider-docker/issues/81)) 200 | 201 | ## 1.0.0 (July 03, 2018) 202 | 203 | NOTES: 204 | * Update `go-dockerclient` to `bf3bc17bb` ([#46](https://github.com/terraform-providers/terraform-provider-docker/pull/46)) 205 | * The `links` property on `resource_docker_container` is now marked as deprecated ([#47](https://github.com/terraform-providers/terraform-provider-docker/pull/47)) 206 | 207 | FEATURES: 208 | * Add `swarm` capabilities ([#29](https://github.com/terraform-providers/terraform-provider-docker/issues/29), [#40](https://github.com/terraform-providers/terraform-provider-docker/pull/40) which fixes [#66](https://github.com/terraform-providers/terraform-provider-docker/pull/66) up to Docker `18.03.1` and API Version `1.37` ([#64](https://github.com/terraform-providers/terraform-provider-docker/issues/64)) 209 | * Add ability to upload executable files [#55](https://github.com/terraform-providers/terraform-provider-docker/pull/55) 210 | * Add support to attach devices to containers [#30](https://github.com/terraform-providers/terraform-provider-docker/issues/30), [#54](https://github.com/terraform-providers/terraform-provider-docker/pull/54) 211 | * Add Ulimits to containers [#35](https://github.com/terraform-providers/terraform-provider-docker/pull/35) 212 | 213 | IMPROVEMENTS: 214 | * Fix `travis` build with a fixed docker version [#57](https://github.com/terraform-providers/terraform-provider-docker/pull/57) 215 | * Infrastructure for Acceptance tests [#39](https://github.com/terraform-providers/terraform-provider-docker/pull/39) 216 | * Internal refactorings [#38](https://github.com/terraform-providers/terraform-provider-docker/pull/38) 217 | * Allow the awslogs log driver [#28](https://github.com/terraform-providers/terraform-provider-docker/pull/28) 218 | * Add prefix `library` only to official images in the path [#27](https://github.com/terraform-providers/terraform-provider-docker/pull/27) 219 | 220 | BUG FIXES 221 | * Update documentation for private registries ([#45](https://github.com/terraform-providers/terraform-provider-docker/issues/45)) 222 | 223 | ## 0.1.1 (November 21, 2017) 224 | 225 | FEATURES: 226 | * Support for pulling images from private registries [#21](https://github.com/terraform-providers/terraform-provider-docker/issues/21) 227 | 228 | ## 0.1.0 (June 20, 2017) 229 | 230 | NOTES: 231 | 232 | * Same functionality as that of Terraform 0.9.8. Repacked as part of [Provider Splitout](https://www.hashicorp.com/blog/upcoming-provider-changes-in-terraform-0-10/) 233 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | TEST?=$$(go list ./... |grep -v 'vendor') 2 | GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) 3 | WEBSITE_REPO=github.com/hashicorp/terraform-website 4 | PKG_NAME=docker 5 | 6 | default: build 7 | 8 | build: fmtcheck 9 | go install 10 | 11 | test: fmtcheck 12 | go test -i $(TEST) || exit 1 13 | echo $(TEST) | \ 14 | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 15 | 16 | testacc_setup: fmtcheck 17 | @sh -c "'$(CURDIR)/scripts/testacc_setup.sh'" 18 | 19 | testacc: fmtcheck 20 | @sh -c "'$(CURDIR)/scripts/testacc_full.sh'" 21 | 22 | testacc_cleanup: fmtcheck 23 | @sh -c "'$(CURDIR)/scripts/testacc_cleanup.sh'" 24 | 25 | compile: fmtcheck 26 | @sh -c "'$(CURDIR)/scripts/compile.sh'" 27 | 28 | vet: 29 | @echo "go vet ." 30 | @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ 31 | echo ""; \ 32 | echo "Vet found suspicious constructs. Please check the reported constructs"; \ 33 | echo "and fix them if necessary before submitting the code for review."; \ 34 | exit 1; \ 35 | fi 36 | 37 | fmt: 38 | gofmt -w $(GOFMT_FILES) 39 | 40 | fmtcheck: 41 | @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" 42 | 43 | errcheck: 44 | @sh -c "'$(CURDIR)/scripts/errcheck.sh'" 45 | 46 | 47 | test-compile: 48 | @if [ "$(TEST)" = "./..." ]; then \ 49 | echo "ERROR: Set TEST to a specific package. For example,"; \ 50 | echo " make test-compile TEST=./$(PKG_NAME)"; \ 51 | exit 1; \ 52 | fi 53 | go test -c $(TEST) $(TESTARGS) 54 | 55 | website: 56 | ifeq (,$(wildcard $(GOPATH)/src/$(WEBSITE_REPO))) 57 | echo "$(WEBSITE_REPO) not found in your GOPATH (necessary for layouts and assets), get-ting..." 58 | git clone https://$(WEBSITE_REPO) $(GOPATH)/src/$(WEBSITE_REPO) 59 | endif 60 | @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) 61 | 62 | website-test: 63 | ifeq (,$(wildcard $(GOPATH)/src/$(WEBSITE_REPO))) 64 | echo "$(WEBSITE_REPO) not found in your GOPATH (necessary for layouts and assets), get-ting..." 65 | git clone https://$(WEBSITE_REPO) $(GOPATH)/src/$(WEBSITE_REPO) 66 | endif 67 | @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider-test PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) 68 | 69 | .PHONY: build test testacc vet fmt fmtcheck errcheck test-compile website website-test 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Please Note: As part of our introduction to self-service publishing in the Terraform Registry, this copy of the provider has been archived, and ownership has been transferred to its active maintainers in the community. Please see the new location on the [Terraform Registry](https://registry.terraform.io/providers/kreuzwerker/docker/latest). You can use the provider from its new location in the Registry by updating your configuration in Terraform with the following: 2 | 3 | ```hcl 4 | terraform { 5 | required_providers { 6 | docker = { 7 | source = "kreuzwerker/docker" 8 | } 9 | } 10 | } 11 | 12 | provider "docker" { 13 | # Configuration options 14 | } 15 | ``` 16 | 17 | Terraform Provider 18 | ================== 19 | 20 | - Website: https://www.terraform.io 21 | - [![Gitter chat](https://badges.gitter.im/hashicorp-terraform/Lobby.png)](https://gitter.im/hashicorp-terraform/Lobby) 22 | - Mailing list: [Google Groups](http://groups.google.com/group/terraform-tool) 23 | 24 | 25 | 26 | Requirements 27 | ------------ 28 | 29 | - [Terraform](https://www.terraform.io/downloads.html) 0.12.x 30 | - [Go](https://golang.org/doc/install) 1.15.x (to build the provider plugin) 31 | 32 | Building The Provider 33 | --------------------- 34 | 35 | Clone repository to: `$GOPATH/src/github.com/terraform-providers/terraform-provider-docker` 36 | 37 | ```sh 38 | $ mkdir -p $GOPATH/src/github.com/terraform-providers; cd $GOPATH/src/github.com/terraform-providers 39 | $ git clone git@github.com:terraform-providers/terraform-provider-docker 40 | ``` 41 | 42 | Enter the provider directory and build the provider 43 | 44 | ```sh 45 | $ cd $GOPATH/src/github.com/terraform-providers/terraform-provider-docker 46 | $ make build 47 | ``` 48 | 49 | Using the provider 50 | ---------------------- 51 | ## Fill in for each provider 52 | 53 | Developing the Provider 54 | --------------------------- 55 | 56 | If you wish to work on the provider, you'll first need the latest version of [Go](http://www.golang.org) installed on your machine (currently 1.15). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH` (note that we typically test older versions of golang as long as they are supported upstream, though we recommend new development to happen on the latest release). 57 | 58 | To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. 59 | 60 | ```sh 61 | $ make build 62 | ... 63 | $ $GOPATH/bin/terraform-provider-docker 64 | ... 65 | ``` 66 | 67 | In order to test the provider, you can simply run `make test`. 68 | 69 | ```sh 70 | $ make test 71 | ``` 72 | 73 | In order to run the full suite of Acceptance tests, run `make testacc`. 74 | 75 | *Note:* Acceptance tests create a local registry which will be deleted afterwards. 76 | 77 | ```sh 78 | $ make testacc 79 | ``` 80 | 81 | In order to run only single Acceptance tests, execute the following steps: 82 | 83 | ```sh 84 | # setup the testing environment 85 | $ make testacc_setup 86 | 87 | # run single tests 88 | TF_LOG=INFO TF_ACC=1 go test -v ./docker -run ^TestAccDockerImage_data_private_config_file$ -timeout 360s 89 | 90 | # cleanup the local testing resources 91 | $ make testacc_cleanup 92 | ``` 93 | 94 | In order to extend the provider and test it with `terraform`, build the provider as mentioned above with 95 | ```sh 96 | $ make build 97 | # or a specific version 98 | $ go build -o terraform-provider-docker_v1.2.0_x4 99 | ``` 100 | 101 | Remove an explicit version of the provider you develop, because `terraform` will fetch 102 | the locally built one in `$GOPATH/bin` 103 | ```hcl 104 | provider "docker" { 105 | # version = "~> 0.1.2" 106 | ... 107 | } 108 | ``` 109 | 110 | 111 | Don't forget to run `terraform init` each time you rebuild the provider. Check [here](https://www.youtube.com/watch?v=TMmovxyo5sY&t=30m14s) for a more detailed explanation. 112 | 113 | You can check the latest released version of a provider at https://releases.hashicorp.com/terraform-provider-docker/. 114 | 115 | Developing on Windows 116 | --------------------- 117 | 118 | You can build and test on Widows without `make`. Run `go install` to 119 | build and `Scripts\runAccTests.bat` to run the test suite. 120 | 121 | Continuous integration for Windows is not available at the moment due 122 | to lack of a CI provider that is free for open source projects *and* 123 | supports running Linux containers in Docker for Windows. For example, 124 | AppVeyor is free for open source projects and provides Docker on its 125 | Windows builds, but only offers Linux containers on Windows as a paid 126 | upgrade. 127 | -------------------------------------------------------------------------------- /docker/config.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "time" 14 | 15 | "github.com/docker/cli/cli/connhelper" 16 | "github.com/docker/docker/api/types" 17 | "github.com/docker/docker/client" 18 | ) 19 | 20 | // Config is the structure that stores the configuration to talk to a 21 | // Docker API compatible host. 22 | type Config struct { 23 | Host string 24 | Ca string 25 | Cert string 26 | Key string 27 | CertPath string 28 | } 29 | 30 | // buildHTTPClientFromBytes builds the http client from bytes (content of the files) 31 | func buildHTTPClientFromBytes(caPEMCert, certPEMBlock, keyPEMBlock []byte) (*http.Client, error) { 32 | tlsConfig := &tls.Config{} 33 | if certPEMBlock != nil && keyPEMBlock != nil { 34 | tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) 35 | if err != nil { 36 | return nil, err 37 | } 38 | tlsConfig.Certificates = []tls.Certificate{tlsCert} 39 | } 40 | 41 | if caPEMCert == nil || len(caPEMCert) == 0 { 42 | tlsConfig.InsecureSkipVerify = true 43 | } else { 44 | caPool := x509.NewCertPool() 45 | if !caPool.AppendCertsFromPEM(caPEMCert) { 46 | return nil, errors.New("Could not add RootCA pem") 47 | } 48 | tlsConfig.RootCAs = caPool 49 | } 50 | 51 | tr := defaultTransport() 52 | tr.TLSClientConfig = tlsConfig 53 | return &http.Client{Transport: tr}, nil 54 | } 55 | 56 | // defaultTransport returns a new http.Transport with similar default values to 57 | // http.DefaultTransport, but with idle connections and keepalives disabled. 58 | func defaultTransport() *http.Transport { 59 | transport := defaultPooledTransport() 60 | transport.DisableKeepAlives = true 61 | transport.MaxIdleConnsPerHost = -1 62 | return transport 63 | } 64 | 65 | // defaultPooledTransport returns a new http.Transport with similar default 66 | // values to http.DefaultTransport. 67 | func defaultPooledTransport() *http.Transport { 68 | transport := &http.Transport{ 69 | Proxy: http.ProxyFromEnvironment, 70 | DialContext: (&net.Dialer{ 71 | Timeout: 30 * time.Second, 72 | KeepAlive: 30 * time.Second, 73 | }).DialContext, 74 | MaxIdleConns: 100, 75 | IdleConnTimeout: 90 * time.Second, 76 | TLSHandshakeTimeout: 10 * time.Second, 77 | ExpectContinueTimeout: 1 * time.Second, 78 | MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, 79 | } 80 | return transport 81 | } 82 | 83 | // NewClient returns a new Docker client. 84 | func (c *Config) NewClient() (*client.Client, error) { 85 | if c.Cert != "" || c.Key != "" { 86 | if c.Cert == "" || c.Key == "" { 87 | return nil, fmt.Errorf("cert_material, and key_material must be specified") 88 | } 89 | 90 | if c.CertPath != "" { 91 | return nil, fmt.Errorf("cert_path must not be specified") 92 | } 93 | 94 | httpClient, err := buildHTTPClientFromBytes([]byte(c.Ca), []byte(c.Cert), []byte(c.Key)) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | // Note: don't change the order here, because the custom client 100 | // needs to be set first them we overwrite the other options: host, version 101 | return client.NewClientWithOpts( 102 | client.WithHTTPClient(httpClient), 103 | client.WithHost(c.Host), 104 | client.WithAPIVersionNegotiation(), 105 | ) 106 | } 107 | 108 | if c.CertPath != "" { 109 | // If there is cert information, load it and use it. 110 | ca := filepath.Join(c.CertPath, "ca.pem") 111 | cert := filepath.Join(c.CertPath, "cert.pem") 112 | key := filepath.Join(c.CertPath, "key.pem") 113 | return client.NewClientWithOpts( 114 | client.WithHost(c.Host), 115 | client.WithTLSClientConfig(ca, cert, key), 116 | client.WithAPIVersionNegotiation(), 117 | ) 118 | } 119 | 120 | // If there is no cert information, then check for ssh:// 121 | helper, err := connhelper.GetConnectionHelper(c.Host) 122 | if err != nil { 123 | return nil, err 124 | } 125 | if helper != nil { 126 | return client.NewClientWithOpts( 127 | client.WithHost(helper.Host), 128 | client.WithDialContext(helper.Dialer), 129 | client.WithAPIVersionNegotiation(), 130 | ) 131 | } 132 | 133 | // If there is no ssh://, then just return the direct client 134 | return client.NewClientWithOpts( 135 | client.WithHost(c.Host), 136 | client.WithAPIVersionNegotiation(), 137 | ) 138 | } 139 | 140 | // Data structure for holding data that we fetch from Docker. 141 | type Data struct { 142 | DockerImages map[string]*types.ImageSummary 143 | } 144 | 145 | // ProviderConfig for the custom registry provider 146 | type ProviderConfig struct { 147 | DockerClient *client.Client 148 | AuthConfigs *AuthConfigs 149 | } 150 | 151 | // The registry address can be referenced in various places (registry auth, docker config file, image name) 152 | // with or without the http(s):// prefix; this function is used to standardize the inputs 153 | func normalizeRegistryAddress(address string) string { 154 | if !strings.HasPrefix(address, "https://") && !strings.HasPrefix(address, "http://") { 155 | return "https://" + address 156 | } 157 | return address 158 | } 159 | -------------------------------------------------------------------------------- /docker/data_source_docker_network.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/docker/docker/api/types" 8 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 9 | ) 10 | 11 | func dataSourceDockerNetwork() *schema.Resource { 12 | return &schema.Resource{ 13 | Read: dataSourceDockerNetworkRead, 14 | 15 | Schema: map[string]*schema.Schema{ 16 | "name": &schema.Schema{ 17 | Type: schema.TypeString, 18 | Optional: true, 19 | }, 20 | 21 | "id": &schema.Schema{ 22 | Type: schema.TypeString, 23 | Optional: true, 24 | }, 25 | 26 | "driver": &schema.Schema{ 27 | Type: schema.TypeString, 28 | Computed: true, 29 | }, 30 | 31 | "options": &schema.Schema{ 32 | Type: schema.TypeMap, 33 | Computed: true, 34 | }, 35 | 36 | "internal": &schema.Schema{ 37 | Type: schema.TypeBool, 38 | Computed: true, 39 | }, 40 | 41 | "ipam_config": &schema.Schema{ 42 | Type: schema.TypeSet, 43 | Computed: true, 44 | Elem: &schema.Resource{ 45 | Schema: map[string]*schema.Schema{ 46 | "subnet": &schema.Schema{ 47 | Type: schema.TypeString, 48 | Optional: true, 49 | ForceNew: true, 50 | }, 51 | 52 | "ip_range": &schema.Schema{ 53 | Type: schema.TypeString, 54 | Optional: true, 55 | ForceNew: true, 56 | }, 57 | 58 | "gateway": &schema.Schema{ 59 | Type: schema.TypeString, 60 | Optional: true, 61 | ForceNew: true, 62 | }, 63 | 64 | "aux_address": &schema.Schema{ 65 | Type: schema.TypeMap, 66 | Optional: true, 67 | ForceNew: true, 68 | }, 69 | }, 70 | }, 71 | }, 72 | 73 | "scope": &schema.Schema{ 74 | Type: schema.TypeString, 75 | Computed: true, 76 | }, 77 | }, 78 | } 79 | } 80 | 81 | type ipamMap map[string]interface{} 82 | 83 | func dataSourceDockerNetworkRead(d *schema.ResourceData, meta interface{}) error { 84 | 85 | name, nameOk := d.GetOk("name") 86 | _, idOk := d.GetOk("id") 87 | 88 | if !nameOk && !idOk { 89 | return fmt.Errorf("One of id or name must be assigned") 90 | } 91 | 92 | client := meta.(*ProviderConfig).DockerClient 93 | 94 | network, err := client.NetworkInspect(context.Background(), name.(string), types.NetworkInspectOptions{}) 95 | 96 | if err != nil { 97 | return fmt.Errorf("Could not find docker network: %s", err) 98 | } 99 | 100 | d.SetId(network.ID) 101 | d.Set("name", network.Name) 102 | d.Set("scope", network.Scope) 103 | d.Set("driver", network.Driver) 104 | d.Set("options", network.Options) 105 | d.Set("internal", network.Internal) 106 | ipam := make([]ipamMap, len(network.IPAM.Config)) 107 | for i, config := range network.IPAM.Config { 108 | ipam[i] = ipamMap{ 109 | "subnet": config.Subnet, 110 | "gateway": config.Gateway, 111 | "aux_address": config.AuxAddress, 112 | "ip_range": config.IPRange, 113 | } 114 | } 115 | err = d.Set("ipam_config", ipam) 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /docker/data_source_docker_network_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 10 | ) 11 | 12 | func TestAccDockerNetworkDataSource_basic(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | Providers: testAccProviders, 16 | Steps: []resource.TestStep{ 17 | resource.TestStep{ 18 | Config: testAccDockerNetworkDataSourceConfig, 19 | Check: resource.ComposeTestCheckFunc( 20 | resource.TestCheckResourceAttr("data.docker_network.bridge", "name", "bridge"), 21 | testAccDockerNetworkDataSourceIPAMRead, 22 | resource.TestCheckResourceAttr("data.docker_network.bridge", "driver", "bridge"), 23 | resource.TestCheckResourceAttr("data.docker_network.bridge", "internal", "false"), 24 | resource.TestCheckResourceAttr("data.docker_network.bridge", "scope", "local"), 25 | ), 26 | }, 27 | }, 28 | }) 29 | } 30 | 31 | func testAccDockerNetworkDataSourceIPAMRead(state *terraform.State) error { 32 | bridge := state.RootModule().Resources["data.docker_network.bridge"] 33 | if bridge == nil { 34 | return fmt.Errorf("unable to find data.docker_network.bridge") 35 | } 36 | attr := bridge.Primary.Attributes["ipam_config.#"] 37 | numberOfReadConfig, err := strconv.Atoi(attr) 38 | if err != nil { 39 | return err 40 | } 41 | if numberOfReadConfig < 1 { 42 | return fmt.Errorf("unable to find any ipam_config") 43 | } 44 | return nil 45 | } 46 | 47 | const testAccDockerNetworkDataSourceConfig = ` 48 | data "docker_network" "bridge" { 49 | name = "bridge" 50 | } 51 | ` 52 | -------------------------------------------------------------------------------- /docker/data_source_docker_registry_image.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 16 | ) 17 | 18 | func dataSourceDockerRegistryImage() *schema.Resource { 19 | return &schema.Resource{ 20 | Read: dataSourceDockerRegistryImageRead, 21 | 22 | Schema: map[string]*schema.Schema{ 23 | "name": { 24 | Type: schema.TypeString, 25 | Optional: true, 26 | }, 27 | 28 | "sha256_digest": { 29 | Type: schema.TypeString, 30 | Computed: true, 31 | }, 32 | }, 33 | } 34 | } 35 | 36 | func dataSourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{}) error { 37 | pullOpts := parseImageOptions(d.Get("name").(string)) 38 | authConfig := meta.(*ProviderConfig).AuthConfigs 39 | 40 | // Use the official Docker Hub if a registry isn't specified 41 | if pullOpts.Registry == "" { 42 | pullOpts.Registry = "registry.hub.docker.com" 43 | } else { 44 | // Otherwise, filter the registry name out of the repo name 45 | pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1) 46 | } 47 | 48 | if pullOpts.Registry == "registry.hub.docker.com" { 49 | // Docker prefixes 'library' to official images in the path; 'consul' becomes 'library/consul' 50 | if !strings.Contains(pullOpts.Repository, "/") { 51 | pullOpts.Repository = "library/" + pullOpts.Repository 52 | } 53 | } 54 | 55 | if pullOpts.Tag == "" { 56 | pullOpts.Tag = "latest" 57 | } 58 | 59 | username := "" 60 | password := "" 61 | 62 | if auth, ok := authConfig.Configs[normalizeRegistryAddress(pullOpts.Registry)]; ok { 63 | username = auth.Username 64 | password = auth.Password 65 | } 66 | 67 | digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, username, password, false) 68 | 69 | if err != nil { 70 | digest, err = getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, username, password, true) 71 | if err != nil { 72 | return fmt.Errorf("Got error when attempting to fetch image version from registry: %s", err) 73 | } 74 | } 75 | 76 | d.SetId(digest) 77 | d.Set("sha256_digest", digest) 78 | 79 | return nil 80 | } 81 | 82 | func getImageDigest(registry, image, tag, username, password string, fallback bool) (string, error) { 83 | client := http.DefaultClient 84 | 85 | // Allow insecure registries only for ACC tests 86 | // cuz we don't have a valid certs for this case 87 | if env, okEnv := os.LookupEnv("TF_ACC"); okEnv { 88 | if i, errConv := strconv.Atoi(env); errConv == nil && i >= 1 { 89 | cfg := &tls.Config{ 90 | InsecureSkipVerify: true, 91 | } 92 | client.Transport = &http.Transport{ 93 | TLSClientConfig: cfg, 94 | } 95 | } 96 | } 97 | 98 | req, err := http.NewRequest("GET", "https://"+registry+"/v2/"+image+"/manifests/"+tag, nil) 99 | if err != nil { 100 | return "", fmt.Errorf("Error creating registry request: %s", err) 101 | } 102 | 103 | if username != "" { 104 | req.SetBasicAuth(username, password) 105 | } 106 | 107 | // We accept schema v2 manifests and manifest lists, and also OCI types 108 | req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json") 109 | req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json") 110 | req.Header.Add("Accept", "application/vnd.oci.image.manifest.v1+json") 111 | req.Header.Add("Accept", "application/vnd.oci.image.index.v1+json") 112 | 113 | if fallback { 114 | // Fallback to this header if the registry does not support the v2 manifest like gcr.io 115 | req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v1+prettyjws") 116 | } 117 | 118 | resp, err := client.Do(req) 119 | 120 | if err != nil { 121 | return "", fmt.Errorf("Error during registry request: %s", err) 122 | } 123 | 124 | switch resp.StatusCode { 125 | // Basic auth was valid or not needed 126 | case http.StatusOK: 127 | return getDigestFromResponse(resp) 128 | 129 | // Either OAuth is required or the basic auth creds were invalid 130 | case http.StatusUnauthorized: 131 | if strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") { 132 | auth := parseAuthHeader(resp.Header.Get("www-authenticate")) 133 | params := url.Values{} 134 | params.Set("service", auth["service"]) 135 | params.Set("scope", auth["scope"]) 136 | tokenRequest, err := http.NewRequest("GET", auth["realm"]+"?"+params.Encode(), nil) 137 | 138 | if err != nil { 139 | return "", fmt.Errorf("Error creating registry request: %s", err) 140 | } 141 | 142 | if username != "" { 143 | tokenRequest.SetBasicAuth(username, password) 144 | } 145 | 146 | tokenResponse, err := client.Do(tokenRequest) 147 | 148 | if err != nil { 149 | return "", fmt.Errorf("Error during registry request: %s", err) 150 | } 151 | 152 | if tokenResponse.StatusCode != http.StatusOK { 153 | return "", fmt.Errorf("Got bad response from registry: " + tokenResponse.Status) 154 | } 155 | 156 | body, err := ioutil.ReadAll(tokenResponse.Body) 157 | if err != nil { 158 | return "", fmt.Errorf("Error reading response body: %s", err) 159 | } 160 | 161 | token := &TokenResponse{} 162 | err = json.Unmarshal(body, token) 163 | if err != nil { 164 | return "", fmt.Errorf("Error parsing OAuth token response: %s", err) 165 | } 166 | 167 | req.Header.Set("Authorization", "Bearer "+token.Token) 168 | digestResponse, err := client.Do(req) 169 | 170 | if err != nil { 171 | return "", fmt.Errorf("Error during registry request: %s", err) 172 | } 173 | 174 | if digestResponse.StatusCode != http.StatusOK { 175 | return "", fmt.Errorf("Got bad response from registry: " + digestResponse.Status) 176 | } 177 | 178 | return getDigestFromResponse(digestResponse) 179 | } 180 | 181 | return "", fmt.Errorf("Bad credentials: " + resp.Status) 182 | 183 | // Some unexpected status was given, return an error 184 | default: 185 | return "", fmt.Errorf("Got bad response from registry: " + resp.Status) 186 | } 187 | } 188 | 189 | type TokenResponse struct { 190 | Token string 191 | } 192 | 193 | // Parses key/value pairs from a WWW-Authenticate header 194 | func parseAuthHeader(header string) map[string]string { 195 | parts := strings.SplitN(header, " ", 2) 196 | parts = strings.Split(parts[1], ",") 197 | opts := make(map[string]string) 198 | 199 | for _, part := range parts { 200 | vals := strings.SplitN(part, "=", 2) 201 | key := vals[0] 202 | val := strings.Trim(vals[1], "\", ") 203 | opts[key] = val 204 | } 205 | 206 | return opts 207 | } 208 | 209 | func getDigestFromResponse(response *http.Response) (string, error) { 210 | header := response.Header.Get("Docker-Content-Digest") 211 | 212 | if header == "" { 213 | body, err := ioutil.ReadAll(response.Body) 214 | if err != nil { 215 | return "", fmt.Errorf("Error reading registry response body: %s", err) 216 | } 217 | 218 | return fmt.Sprintf("sha256:%x", sha256.Sum256(body)), nil 219 | } 220 | 221 | return header, nil 222 | } 223 | -------------------------------------------------------------------------------- /docker/data_source_docker_registry_image_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 12 | ) 13 | 14 | var registryDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) 15 | 16 | func TestAccDockerRegistryImage_basic(t *testing.T) { 17 | resource.Test(t, resource.TestCase{ 18 | PreCheck: func() { testAccPreCheck(t) }, 19 | Providers: testAccProviders, 20 | Steps: []resource.TestStep{ 21 | { 22 | Config: testAccDockerImageDataSourceConfig, 23 | Check: resource.ComposeTestCheckFunc( 24 | resource.TestMatchResourceAttr("data.docker_registry_image.foo", "sha256_digest", registryDigestRegexp), 25 | ), 26 | }, 27 | }, 28 | }) 29 | } 30 | 31 | func TestAccDockerRegistryImage_private(t *testing.T) { 32 | resource.Test(t, resource.TestCase{ 33 | PreCheck: func() { testAccPreCheck(t) }, 34 | Providers: testAccProviders, 35 | Steps: []resource.TestStep{ 36 | { 37 | Config: testAccDockerImageDataSourcePrivateConfig, 38 | Check: resource.ComposeTestCheckFunc( 39 | resource.TestMatchResourceAttr("data.docker_registry_image.bar", "sha256_digest", registryDigestRegexp), 40 | ), 41 | }, 42 | }, 43 | }) 44 | } 45 | 46 | func TestAccDockerRegistryImage_auth(t *testing.T) { 47 | registry := "127.0.0.1:15000" 48 | image := "127.0.0.1:15000/tftest-service:v1" 49 | resource.Test(t, resource.TestCase{ 50 | PreCheck: func() { testAccPreCheck(t) }, 51 | Providers: testAccProviders, 52 | Steps: []resource.TestStep{ 53 | { 54 | Config: fmt.Sprintf(testAccDockerImageDataSourceAuthConfig, registry, image), 55 | Check: resource.ComposeTestCheckFunc( 56 | resource.TestMatchResourceAttr("data.docker_registry_image.foobar", "sha256_digest", registryDigestRegexp), 57 | ), 58 | }, 59 | }, 60 | CheckDestroy: checkAndRemoveImages, 61 | }) 62 | } 63 | 64 | const testAccDockerImageDataSourceConfig = ` 65 | data "docker_registry_image" "foo" { 66 | name = "alpine:latest" 67 | } 68 | ` 69 | 70 | const testAccDockerImageDataSourcePrivateConfig = ` 71 | data "docker_registry_image" "bar" { 72 | name = "gcr.io:443/google_containers/pause:0.8.0" 73 | } 74 | ` 75 | 76 | const testAccDockerImageDataSourceAuthConfig = ` 77 | provider "docker" { 78 | alias = "private" 79 | registry_auth { 80 | address = "%s" 81 | } 82 | } 83 | data "docker_registry_image" "foobar" { 84 | provider = "docker.private" 85 | name = "%s" 86 | } 87 | ` 88 | 89 | func TestGetDigestFromResponse(t *testing.T) { 90 | headerContent := "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 91 | respWithHeaders := &http.Response{ 92 | Header: http.Header{ 93 | "Docker-Content-Digest": []string{headerContent}, 94 | }, 95 | Body: ioutil.NopCloser(bytes.NewReader([]byte("foo"))), 96 | } 97 | 98 | if digest, _ := getDigestFromResponse(respWithHeaders); digest != headerContent { 99 | t.Errorf("Expected digest from header to be %s, but was %s", headerContent, digest) 100 | } 101 | 102 | bodyDigest := "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9" 103 | respWithoutHeaders := &http.Response{ 104 | Header: make(http.Header), 105 | Body: ioutil.NopCloser(bytes.NewReader([]byte("bar"))), 106 | } 107 | 108 | if digest, _ := getDigestFromResponse(respWithoutHeaders); digest != bodyDigest { 109 | t.Errorf("Expected digest calculated from body to be %s, but was %s", bodyDigest, digest) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docker/label.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 8 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 10 | ) 11 | 12 | func labelToPair(label map[string]interface{}) (string, string) { 13 | return label["label"].(string), label["value"].(string) 14 | } 15 | 16 | func labelSetToMap(labels *schema.Set) map[string]string { 17 | labelsSlice := labels.List() 18 | 19 | mapped := make(map[string]string, len(labelsSlice)) 20 | for _, label := range labelsSlice { 21 | l, v := labelToPair(label.(map[string]interface{})) 22 | mapped[l] = v 23 | } 24 | return mapped 25 | } 26 | 27 | func hashLabel(v interface{}) int { 28 | labelMap := v.(map[string]interface{}) 29 | return hashStringLabel(labelMap["label"].(string)) 30 | } 31 | 32 | func hashStringLabel(str string) int { 33 | return schema.HashString(str) 34 | } 35 | 36 | func mapStringInterfaceToLabelList(labels map[string]interface{}) []interface{} { 37 | var mapped []interface{} 38 | for k, v := range labels { 39 | mapped = append(mapped, map[string]interface{}{ 40 | "label": k, 41 | "value": fmt.Sprintf("%v", v), 42 | }) 43 | } 44 | return mapped 45 | } 46 | 47 | func mapToLabelSet(labels map[string]string) *schema.Set { 48 | var mapped []interface{} 49 | for k, v := range labels { 50 | mapped = append(mapped, map[string]interface{}{ 51 | "label": k, 52 | "value": v, 53 | }) 54 | } 55 | return schema.NewSet(hashLabel, mapped) 56 | } 57 | 58 | var labelSchema = &schema.Resource{ 59 | Schema: map[string]*schema.Schema{ 60 | "label": &schema.Schema{ 61 | Type: schema.TypeString, 62 | Description: "Name of the label", 63 | Required: true, 64 | }, 65 | "value": &schema.Schema{ 66 | Type: schema.TypeString, 67 | Description: "Value of the label", 68 | Required: true, 69 | }, 70 | }, 71 | } 72 | 73 | //gatherImmediateSubkeys given an incomplete attribute identifier, find all 74 | //the strings (if any) that appear after this one in the various dot-separated 75 | //identifiers. 76 | func gatherImmediateSubkeys(attrs map[string]string, partialKey string) []string { 77 | var immediateSubkeys = []string{} 78 | for k := range attrs { 79 | prefix := partialKey + "." 80 | if strings.HasPrefix(k, prefix) { 81 | rest := strings.TrimPrefix(k, prefix) 82 | parts := strings.SplitN(rest, ".", 2) 83 | immediateSubkeys = append(immediateSubkeys, parts[0]) 84 | } 85 | } 86 | 87 | return immediateSubkeys 88 | } 89 | 90 | func getLabelMapForPartialKey(attrs map[string]string, partialKey string) map[string]string { 91 | setIDs := gatherImmediateSubkeys(attrs, partialKey) 92 | 93 | var labelMap = map[string]string{} 94 | for _, id := range setIDs { 95 | if id == "#" { 96 | continue 97 | } 98 | prefix := partialKey + "." + id 99 | labelMap[attrs[prefix+".label"]] = attrs[prefix+".value"] 100 | } 101 | 102 | return labelMap 103 | } 104 | 105 | func testCheckLabelMap(name string, partialKey string, expectedLabels map[string]string) resource.TestCheckFunc { 106 | return func(s *terraform.State) error { 107 | attrs := s.RootModule().Resources[name].Primary.Attributes 108 | labelMap := getLabelMapForPartialKey(attrs, partialKey) 109 | 110 | if len(labelMap) != len(expectedLabels) { 111 | return fmt.Errorf("expected %v labels, found %v", len(expectedLabels), len(labelMap)) 112 | } 113 | 114 | for l, v := range expectedLabels { 115 | if labelMap[l] != v { 116 | return fmt.Errorf("expected value %v for label %v, got %v", v, l, labelMap[v]) 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /docker/label_migration.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | func replaceLabelsMapFieldWithSetField(rawState map[string]interface{}) map[string]interface{} { 4 | labelMapIFace := rawState["labels"] 5 | if labelMapIFace != nil { 6 | labelMap := labelMapIFace.(map[string]interface{}) 7 | rawState["labels"] = mapStringInterfaceToLabelList(labelMap) 8 | } else { 9 | rawState["labels"] = []interface{}{} 10 | } 11 | 12 | return rawState 13 | } 14 | 15 | func migrateContainerLabels(rawState map[string]interface{}) map[string]interface{} { 16 | replaceLabelsMapFieldWithSetField(rawState) 17 | 18 | m, ok := rawState["mounts"] 19 | if !ok || m == nil { 20 | // https://github.com/terraform-providers/terraform-provider-docker/issues/264 21 | rawState["mounts"] = []interface{}{} 22 | return rawState 23 | } 24 | 25 | mounts := m.([]interface{}) 26 | newMounts := make([]interface{}, len(mounts)) 27 | for i, mountI := range mounts { 28 | mount := mountI.(map[string]interface{}) 29 | volumeOptionsList := mount["volume_options"].([]interface{}) 30 | 31 | if len(volumeOptionsList) != 0 { 32 | replaceLabelsMapFieldWithSetField(volumeOptionsList[0].(map[string]interface{})) 33 | } 34 | newMounts[i] = mount 35 | } 36 | rawState["mounts"] = newMounts 37 | 38 | return rawState 39 | } 40 | 41 | func migrateServiceLabels(rawState map[string]interface{}) map[string]interface{} { 42 | replaceLabelsMapFieldWithSetField(rawState) 43 | 44 | taskSpec := rawState["task_spec"].([]interface{})[0].(map[string]interface{}) 45 | containerSpec := taskSpec["container_spec"].([]interface{})[0].(map[string]interface{}) 46 | migrateContainerLabels(containerSpec) 47 | 48 | return rawState 49 | } 50 | -------------------------------------------------------------------------------- /docker/label_migration_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 8 | ) 9 | 10 | func TestMigrateServiceLabelState_empty_labels(t *testing.T) { 11 | v0State := map[string]interface{}{ 12 | "name": "volume-name", 13 | "task_spec": []interface{}{ 14 | map[string]interface{}{ 15 | "container_spec": []interface{}{ 16 | map[string]interface{}{ 17 | "image": "repo:tag", 18 | "mounts": []interface{}{ 19 | map[string]interface{}{ 20 | "target": "path/to/target", 21 | "type": "bind", 22 | "volume_options": []interface{}{ 23 | map[string]interface{}{}, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | } 32 | 33 | //first validate that we build that correctly 34 | v0Config := terraform.NewResourceConfigRaw(v0State) 35 | warns, errs := resourceDockerServiceV0().Validate(v0Config) 36 | if len(warns) > 0 || len(errs) > 0 { 37 | t.Error("test precondition failed - attempt to migrate an invalid v0 config") 38 | return 39 | } 40 | 41 | v1State := migrateServiceLabels(v0State) 42 | v1Config := terraform.NewResourceConfigRaw(v1State) 43 | warns, errs = resourceDockerService().Validate(v1Config) 44 | if len(warns) > 0 || len(errs) > 0 { 45 | fmt.Println(warns, errs) 46 | t.Error("migrated service config is invalid") 47 | return 48 | } 49 | } 50 | 51 | func TestMigrateServiceLabelState_with_labels(t *testing.T) { 52 | v0State := map[string]interface{}{ 53 | "name": "volume-name", 54 | "task_spec": []interface{}{ 55 | map[string]interface{}{ 56 | "container_spec": []interface{}{ 57 | map[string]interface{}{ 58 | "image": "repo:tag", 59 | "labels": map[string]interface{}{ 60 | "type": "container", 61 | "env": "dev", 62 | }, 63 | "mounts": []interface{}{ 64 | map[string]interface{}{ 65 | "target": "path/to/target", 66 | "type": "bind", 67 | "volume_options": []interface{}{ 68 | map[string]interface{}{ 69 | "labels": map[string]interface{}{ 70 | "type": "mount", 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | "labels": map[string]interface{}{ 81 | "foo": "bar", 82 | "env": "dev", 83 | }, 84 | } 85 | 86 | //first validate that we build that correctly 87 | v0Config := terraform.NewResourceConfigRaw(v0State) 88 | warns, errs := resourceDockerServiceV0().Validate(v0Config) 89 | if len(warns) > 0 || len(errs) > 0 { 90 | t.Error("test precondition failed - attempt to migrate an invalid v0 config") 91 | return 92 | } 93 | 94 | v1State := migrateServiceLabels(v0State) 95 | v1Config := terraform.NewResourceConfigRaw(v1State) 96 | warns, errs = resourceDockerService().Validate(v1Config) 97 | if len(warns) > 0 || len(errs) > 0 { 98 | fmt.Println(warns, errs) 99 | t.Error("migrated service config is invalid") 100 | return 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docker/provider.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/user" 10 | "strings" 11 | 12 | "github.com/docker/cli/cli/config/configfile" 13 | "github.com/docker/docker/api/types" 14 | 15 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 16 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 17 | ) 18 | 19 | // Provider creates the Docker provider 20 | func Provider() terraform.ResourceProvider { 21 | return &schema.Provider{ 22 | Schema: map[string]*schema.Schema{ 23 | "host": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_HOST", "unix:///var/run/docker.sock"), 27 | Description: "The Docker daemon address", 28 | }, 29 | 30 | "ca_material": { 31 | Type: schema.TypeString, 32 | Optional: true, 33 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_CA_MATERIAL", ""), 34 | Description: "PEM-encoded content of Docker host CA certificate", 35 | }, 36 | "cert_material": { 37 | Type: schema.TypeString, 38 | Optional: true, 39 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_MATERIAL", ""), 40 | Description: "PEM-encoded content of Docker client certificate", 41 | }, 42 | "key_material": { 43 | Type: schema.TypeString, 44 | Optional: true, 45 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_KEY_MATERIAL", ""), 46 | Description: "PEM-encoded content of Docker client private key", 47 | }, 48 | 49 | "cert_path": { 50 | Type: schema.TypeString, 51 | Optional: true, 52 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_CERT_PATH", ""), 53 | Description: "Path to directory with Docker TLS config", 54 | }, 55 | 56 | "registry_auth": { 57 | Type: schema.TypeSet, 58 | Optional: true, 59 | Elem: &schema.Resource{ 60 | Schema: map[string]*schema.Schema{ 61 | "address": { 62 | Type: schema.TypeString, 63 | Required: true, 64 | Description: "Address of the registry", 65 | }, 66 | 67 | "username": { 68 | Type: schema.TypeString, 69 | Optional: true, 70 | ConflictsWith: []string{"registry_auth.config_file", "registry_auth.config_file_content"}, 71 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_USER", ""), 72 | Description: "Username for the registry", 73 | }, 74 | 75 | "password": { 76 | Type: schema.TypeString, 77 | Optional: true, 78 | Sensitive: true, 79 | ConflictsWith: []string{"registry_auth.config_file", "registry_auth.config_file_content"}, 80 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_REGISTRY_PASS", ""), 81 | Description: "Password for the registry", 82 | }, 83 | 84 | "config_file": { 85 | Type: schema.TypeString, 86 | Optional: true, 87 | ConflictsWith: []string{"registry_auth.username", "registry_auth.password", "registry_auth.config_file_content"}, 88 | DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONFIG", "~/.docker/config.json"), 89 | Description: "Path to docker json file for registry auth", 90 | }, 91 | 92 | "config_file_content": { 93 | Type: schema.TypeString, 94 | Optional: true, 95 | ConflictsWith: []string{"registry_auth.username", "registry_auth.password", "registry_auth.config_file"}, 96 | Description: "Plain content of the docker json file for registry auth", 97 | }, 98 | }, 99 | }, 100 | }, 101 | }, 102 | 103 | ResourcesMap: map[string]*schema.Resource{ 104 | "docker_container": resourceDockerContainer(), 105 | "docker_image": resourceDockerImage(), 106 | "docker_registry_image": resourceDockerRegistryImage(), 107 | "docker_network": resourceDockerNetwork(), 108 | "docker_volume": resourceDockerVolume(), 109 | "docker_config": resourceDockerConfig(), 110 | "docker_secret": resourceDockerSecret(), 111 | "docker_service": resourceDockerService(), 112 | }, 113 | 114 | DataSourcesMap: map[string]*schema.Resource{ 115 | "docker_registry_image": dataSourceDockerRegistryImage(), 116 | "docker_network": dataSourceDockerNetwork(), 117 | }, 118 | 119 | ConfigureFunc: providerConfigure, 120 | } 121 | } 122 | 123 | func providerConfigure(d *schema.ResourceData) (interface{}, error) { 124 | config := Config{ 125 | Host: d.Get("host").(string), 126 | Ca: d.Get("ca_material").(string), 127 | Cert: d.Get("cert_material").(string), 128 | Key: d.Get("key_material").(string), 129 | CertPath: d.Get("cert_path").(string), 130 | } 131 | 132 | client, err := config.NewClient() 133 | if err != nil { 134 | return nil, fmt.Errorf("Error initializing Docker client: %s", err) 135 | } 136 | 137 | ctx := context.Background() 138 | _, err = client.Ping(ctx) 139 | if err != nil { 140 | return nil, fmt.Errorf("Error pinging Docker server: %s", err) 141 | } 142 | 143 | authConfigs := &AuthConfigs{} 144 | 145 | if v, ok := d.GetOk("registry_auth"); ok { // TODO load them anyway 146 | authConfigs, err = providerSetToRegistryAuth(v.(*schema.Set)) 147 | 148 | if err != nil { 149 | return nil, fmt.Errorf("Error loading registry auth config: %s", err) 150 | } 151 | } 152 | 153 | providerConfig := ProviderConfig{ 154 | DockerClient: client, 155 | AuthConfigs: authConfigs, 156 | } 157 | 158 | return &providerConfig, nil 159 | } 160 | 161 | // AuthConfigs represents authentication options to use for the 162 | // PushImage method accommodating the new X-Registry-Config header 163 | type AuthConfigs struct { 164 | Configs map[string]types.AuthConfig `json:"configs"` 165 | } 166 | 167 | // Take the given registry_auth schemas and return a map of registry auth configurations 168 | func providerSetToRegistryAuth(authSet *schema.Set) (*AuthConfigs, error) { 169 | authConfigs := AuthConfigs{ 170 | Configs: make(map[string]types.AuthConfig), 171 | } 172 | 173 | for _, authInt := range authSet.List() { 174 | auth := authInt.(map[string]interface{}) 175 | authConfig := types.AuthConfig{} 176 | authConfig.ServerAddress = normalizeRegistryAddress(auth["address"].(string)) 177 | registryHostname := convertToHostname(authConfig.ServerAddress) 178 | 179 | // For each registry_auth block, generate an AuthConfiguration using either 180 | // username/password or the given config file 181 | if username, ok := auth["username"]; ok && username.(string) != "" { 182 | log.Println("[DEBUG] Using username for registry auths:", username) 183 | authConfig.Username = auth["username"].(string) 184 | authConfig.Password = auth["password"].(string) 185 | 186 | // Note: check for config_file_content first because config_file has a default which would be used 187 | // nevertheless config_file_content is set or not. The default has to be kept to check for the 188 | // environment variable and to be backwards compatible 189 | } else if configFileContent, ok := auth["config_file_content"]; ok && configFileContent.(string) != "" { 190 | log.Println("[DEBUG] Parsing file content for registry auths:", configFileContent.(string)) 191 | r := strings.NewReader(configFileContent.(string)) 192 | 193 | c, err := loadConfigFile(r) 194 | if err != nil { 195 | return nil, fmt.Errorf("Error parsing docker registry config json: %v", err) 196 | } 197 | authFileConfig, err := c.GetAuthConfig(registryHostname) 198 | if err != nil { 199 | return nil, fmt.Errorf("Couldn't find registry config for '%s' in file content", registryHostname) 200 | } 201 | authConfig.Username = authFileConfig.Username 202 | authConfig.Password = authFileConfig.Password 203 | 204 | // As last step we check if a config file path is given 205 | } else if configFile, ok := auth["config_file"]; ok && configFile.(string) != "" { 206 | filePath := configFile.(string) 207 | log.Println("[DEBUG] Parsing file for registry auths:", filePath) 208 | 209 | // We manually expand the path and do not use the 'pathexpand' interpolation function 210 | // because in the default of this varable we refer to '~/.docker/config.json' 211 | if strings.HasPrefix(filePath, "~/") { 212 | usr, err := user.Current() 213 | if err != nil { 214 | return nil, err 215 | } 216 | filePath = strings.Replace(filePath, "~", usr.HomeDir, 1) 217 | } 218 | r, err := os.Open(filePath) 219 | if err != nil { 220 | continue 221 | } 222 | c, err := loadConfigFile(r) 223 | if err != nil { 224 | continue 225 | } 226 | authFileConfig, err := c.GetAuthConfig(registryHostname) 227 | if err != nil { 228 | continue 229 | } 230 | authConfig.Username = authFileConfig.Username 231 | authConfig.Password = authFileConfig.Password 232 | } 233 | 234 | authConfigs.Configs[authConfig.ServerAddress] = authConfig 235 | } 236 | 237 | return &authConfigs, nil 238 | } 239 | 240 | func loadConfigFile(configData io.Reader) (*configfile.ConfigFile, error) { 241 | configFile := configfile.New("") 242 | if err := configFile.LoadFromReader(configData); err != nil { 243 | log.Println("[DEBUG] Error parsing registry config: ", err) 244 | log.Println("[DEBUG] Will try parsing from legacy format") 245 | if err := configFile.LegacyLoadFromReader(configData); err != nil { 246 | return nil, err 247 | } 248 | } 249 | return configFile, nil 250 | } 251 | 252 | // ConvertToHostname converts a registry url which has http|https prepended 253 | // to just an hostname. 254 | // Copied from github.com/docker/docker/registry.ConvertToHostname to reduce dependencies. 255 | func convertToHostname(url string) string { 256 | stripped := url 257 | if strings.HasPrefix(url, "http://") { 258 | stripped = strings.TrimPrefix(url, "http://") 259 | } else if strings.HasPrefix(url, "https://") { 260 | stripped = strings.TrimPrefix(url, "https://") 261 | } 262 | 263 | nameParts := strings.SplitN(stripped, "/", 2) 264 | 265 | return nameParts[0] 266 | } 267 | -------------------------------------------------------------------------------- /docker/provider_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os/exec" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 11 | ) 12 | 13 | var testAccProviders map[string]terraform.ResourceProvider 14 | var testAccProvider *schema.Provider 15 | 16 | func init() { 17 | testAccProvider = Provider().(*schema.Provider) 18 | testAccProviders = map[string]terraform.ResourceProvider{ 19 | "docker": testAccProvider, 20 | } 21 | } 22 | 23 | func TestProvider(t *testing.T) { 24 | if err := Provider().(*schema.Provider).InternalValidate(); err != nil { 25 | t.Fatalf("err: %s", err) 26 | } 27 | } 28 | 29 | func TestAccDockerProvider_WithIncompleteRegistryAuth(t *testing.T) { 30 | resource.Test(t, resource.TestCase{ 31 | PreCheck: func() { testAccPreCheck(t) }, 32 | Providers: testAccProviders, 33 | Steps: []resource.TestStep{ 34 | { 35 | Config: testAccDockerProviderWithIncompleteAuthConfig, 36 | ExpectError: regexp.MustCompile(`401 Unauthorized`), 37 | }, 38 | }, 39 | }) 40 | } 41 | 42 | func TestProvider_impl(t *testing.T) { 43 | var _ terraform.ResourceProvider = Provider() 44 | } 45 | 46 | func testAccPreCheck(t *testing.T) { 47 | cmd := exec.Command("docker", "version") 48 | if err := cmd.Run(); err != nil { 49 | t.Fatalf("Docker must be available: %s", err) 50 | } 51 | 52 | cmd = exec.Command("docker", "node", "ls") 53 | if err := cmd.Run(); err != nil { 54 | cmd = exec.Command("docker", "swarm", "init") 55 | if err := cmd.Run(); err != nil { 56 | t.Fatalf("Docker swarm could not be initialized: %s", err) 57 | } 58 | } 59 | 60 | err := testAccProvider.Configure(terraform.NewResourceConfigRaw(nil)) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | } 65 | 66 | const testAccDockerProviderWithIncompleteAuthConfig = ` 67 | provider "docker" { 68 | alias = "private" 69 | registry_auth { 70 | address = "" 71 | username = "" 72 | password = "" 73 | } 74 | } 75 | data "docker_registry_image" "foobar" { 76 | provider = "docker.private" 77 | name = "localhost:15000/helloworld:1.0" 78 | } 79 | ` 80 | -------------------------------------------------------------------------------- /docker/resource_docker_config.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/base64" 5 | "log" 6 | 7 | "context" 8 | 9 | "github.com/docker/docker/api/types/swarm" 10 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 11 | ) 12 | 13 | func resourceDockerConfig() *schema.Resource { 14 | return &schema.Resource{ 15 | Create: resourceDockerConfigCreate, 16 | Read: resourceDockerConfigRead, 17 | Delete: resourceDockerConfigDelete, 18 | Importer: &schema.ResourceImporter{ 19 | State: schema.ImportStatePassthrough, 20 | }, 21 | 22 | Schema: map[string]*schema.Schema{ 23 | "name": { 24 | Type: schema.TypeString, 25 | Description: "User-defined name of the config", 26 | Required: true, 27 | ForceNew: true, 28 | }, 29 | 30 | "data": { 31 | Type: schema.TypeString, 32 | Description: "Base64-url-safe-encoded config data", 33 | Required: true, 34 | Sensitive: true, 35 | ForceNew: true, 36 | ValidateFunc: validateStringIsBase64Encoded(), 37 | }, 38 | }, 39 | } 40 | } 41 | 42 | func resourceDockerConfigCreate(d *schema.ResourceData, meta interface{}) error { 43 | client := meta.(*ProviderConfig).DockerClient 44 | data, _ := base64.StdEncoding.DecodeString(d.Get("data").(string)) 45 | 46 | configSpec := swarm.ConfigSpec{ 47 | Annotations: swarm.Annotations{ 48 | Name: d.Get("name").(string), 49 | }, 50 | Data: data, 51 | } 52 | 53 | config, err := client.ConfigCreate(context.Background(), configSpec) 54 | if err != nil { 55 | return err 56 | } 57 | d.SetId(config.ID) 58 | 59 | return resourceDockerConfigRead(d, meta) 60 | } 61 | 62 | func resourceDockerConfigRead(d *schema.ResourceData, meta interface{}) error { 63 | client := meta.(*ProviderConfig).DockerClient 64 | config, _, err := client.ConfigInspectWithRaw(context.Background(), d.Id()) 65 | 66 | if err != nil { 67 | log.Printf("[WARN] Config (%s) not found, removing from state", d.Id()) 68 | d.SetId("") 69 | return nil 70 | } 71 | d.SetId(config.ID) 72 | d.Set("name", config.Spec.Name) 73 | d.Set("data", base64.StdEncoding.EncodeToString(config.Spec.Data)) 74 | return nil 75 | } 76 | 77 | func resourceDockerConfigDelete(d *schema.ResourceData, meta interface{}) error { 78 | client := meta.(*ProviderConfig).DockerClient 79 | err := client.ConfigRemove(context.Background(), d.Id()) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | d.SetId("") 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /docker/resource_docker_config_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "context" 8 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 10 | ) 11 | 12 | func TestAccDockerConfig_basic(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | Providers: testAccProviders, 16 | CheckDestroy: testCheckDockerConfigDestroy, 17 | Steps: []resource.TestStep{ 18 | { 19 | Config: ` 20 | resource "docker_config" "foo" { 21 | name = "foo-config" 22 | data = "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA==" 23 | } 24 | `, 25 | Check: resource.ComposeTestCheckFunc( 26 | resource.TestCheckResourceAttr("docker_config.foo", "name", "foo-config"), 27 | resource.TestCheckResourceAttr("docker_config.foo", "data", "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA=="), 28 | ), 29 | }, 30 | { 31 | ResourceName: "docker_config.foo", 32 | ImportState: true, 33 | ImportStateVerify: true, 34 | }, 35 | }, 36 | }) 37 | } 38 | func TestAccDockerConfig_basicUpdatable(t *testing.T) { 39 | resource.Test(t, resource.TestCase{ 40 | PreCheck: func() { testAccPreCheck(t) }, 41 | Providers: testAccProviders, 42 | CheckDestroy: testCheckDockerConfigDestroy, 43 | Steps: []resource.TestStep{ 44 | { 45 | Config: ` 46 | resource "docker_config" "foo" { 47 | name = "tftest-myconfig-${replace(timestamp(),":", ".")}" 48 | data = "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA==" 49 | 50 | lifecycle { 51 | ignore_changes = ["name"] 52 | create_before_destroy = true 53 | } 54 | } 55 | `, 56 | Check: resource.ComposeTestCheckFunc( 57 | resource.TestCheckResourceAttr("docker_config.foo", "data", "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA=="), 58 | ), 59 | }, 60 | { 61 | Config: ` 62 | resource "docker_config" "foo" { 63 | name = "tftest-myconfig2-${replace(timestamp(),":", ".")}" 64 | data = "U3VuIDI1IE1hciAyMDE4IDE0OjQ2OjE5IENFU1QK" 65 | 66 | lifecycle { 67 | ignore_changes = ["name"] 68 | create_before_destroy = true 69 | } 70 | } 71 | `, 72 | Check: resource.ComposeTestCheckFunc( 73 | resource.TestCheckResourceAttr("docker_config.foo", "data", "U3VuIDI1IE1hciAyMDE4IDE0OjQ2OjE5IENFU1QK"), 74 | ), 75 | }, 76 | { 77 | ResourceName: "docker_config.foo", 78 | ImportState: true, 79 | ImportStateVerify: true, 80 | }, 81 | }, 82 | }) 83 | } 84 | 85 | ///////////// 86 | // Helpers 87 | ///////////// 88 | func testCheckDockerConfigDestroy(s *terraform.State) error { 89 | client := testAccProvider.Meta().(*ProviderConfig).DockerClient 90 | for _, rs := range s.RootModule().Resources { 91 | if rs.Type != "configs" { 92 | continue 93 | } 94 | 95 | id := rs.Primary.Attributes["id"] 96 | _, _, err := client.ConfigInspectWithRaw(context.Background(), id) 97 | 98 | if err == nil { 99 | return fmt.Errorf("Config with id '%s' still exists", id) 100 | } 101 | return nil 102 | } 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /docker/resource_docker_container_migrate.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sort" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 10 | ) 11 | 12 | func resourceDockerContainerMigrateState( 13 | v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { 14 | switch v { 15 | case 0: 16 | log.Println("[INFO] Found Docker Container State v0; migrating to v1") 17 | return migrateDockerContainerMigrateStateV0toV1(is, meta) 18 | default: 19 | return is, fmt.Errorf("Unexpected schema version: %d", v) 20 | } 21 | } 22 | 23 | func migrateDockerContainerMigrateStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { 24 | if is.Empty() { 25 | log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") 26 | return is, nil 27 | } 28 | 29 | log.Printf("[DEBUG] Docker Container Attributes before Migration: %#v", is.Attributes) 30 | 31 | err := updateV0ToV1PortsOrder(is, meta) 32 | 33 | log.Printf("[DEBUG] Docker Container Attributes after State Migration: %#v", is.Attributes) 34 | 35 | return is, err 36 | } 37 | 38 | type mappedPort struct { 39 | internal int 40 | external int 41 | ip string 42 | protocol string 43 | } 44 | 45 | type byPort []mappedPort 46 | 47 | func (s byPort) Len() int { 48 | return len(s) 49 | } 50 | func (s byPort) Swap(i, j int) { 51 | s[i], s[j] = s[j], s[i] 52 | } 53 | func (s byPort) Less(i, j int) bool { 54 | return s[i].internal < s[j].internal 55 | } 56 | 57 | func updateV0ToV1PortsOrder(is *terraform.InstanceState, meta interface{}) error { 58 | reader := &schema.MapFieldReader{ 59 | Schema: resourceDockerContainer().Schema, 60 | Map: schema.BasicMapReader(is.Attributes), 61 | } 62 | 63 | writer := &schema.MapFieldWriter{ 64 | Schema: resourceDockerContainer().Schema, 65 | } 66 | 67 | result, err := reader.ReadField([]string{"ports"}) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if result.Value == nil { 73 | return nil 74 | } 75 | 76 | // map the ports into a struct, so they can be sorted easily 77 | portsMapped := make([]mappedPort, 0) 78 | portsRaw := result.Value.([]interface{}) 79 | for _, portRaw := range portsRaw { 80 | if portRaw == nil { 81 | continue 82 | } 83 | portTyped := portRaw.(map[string]interface{}) 84 | portMapped := mappedPort{ 85 | internal: portTyped["internal"].(int), 86 | external: portTyped["external"].(int), 87 | ip: portTyped["ip"].(string), 88 | protocol: portTyped["protocol"].(string), 89 | } 90 | 91 | portsMapped = append(portsMapped, portMapped) 92 | } 93 | sort.Sort(byPort(portsMapped)) 94 | 95 | // map the sorted ports to an output structure tf can write 96 | outputPorts := make([]interface{}, 0) 97 | for _, mappedPort := range portsMapped { 98 | outputPort := make(map[string]interface{}, 0) 99 | outputPort["internal"] = mappedPort.internal 100 | outputPort["external"] = mappedPort.external 101 | outputPort["ip"] = mappedPort.ip 102 | outputPort["protocol"] = mappedPort.protocol 103 | outputPorts = append(outputPorts, outputPort) 104 | } 105 | 106 | // store them back to state 107 | if err := writer.WriteField([]string{"ports"}, outputPorts); err != nil { 108 | return err 109 | } 110 | for k, v := range writer.Map() { 111 | is.Attributes[k] = v 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /docker/resource_docker_image.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 5 | ) 6 | 7 | func resourceDockerImage() *schema.Resource { 8 | return &schema.Resource{ 9 | Create: resourceDockerImageCreate, 10 | Read: resourceDockerImageRead, 11 | Update: resourceDockerImageUpdate, 12 | Delete: resourceDockerImageDelete, 13 | 14 | Schema: map[string]*schema.Schema{ 15 | "name": { 16 | Type: schema.TypeString, 17 | Required: true, 18 | }, 19 | 20 | "latest": { 21 | Type: schema.TypeString, 22 | Computed: true, 23 | }, 24 | 25 | "keep_locally": { 26 | Type: schema.TypeBool, 27 | Optional: true, 28 | }, 29 | 30 | "pull_trigger": { 31 | Type: schema.TypeString, 32 | Optional: true, 33 | ForceNew: true, 34 | ConflictsWith: []string{"pull_triggers"}, 35 | Deprecated: "Use field pull_triggers instead", 36 | }, 37 | 38 | "pull_triggers": { 39 | Type: schema.TypeSet, 40 | Optional: true, 41 | ForceNew: true, 42 | Elem: &schema.Schema{Type: schema.TypeString}, 43 | Set: schema.HashString, 44 | }, 45 | 46 | "output": { 47 | Type: schema.TypeString, 48 | Computed: true, 49 | Elem: &schema.Schema{ 50 | Type: schema.TypeString, 51 | }, 52 | }, 53 | 54 | "build": { 55 | Type: schema.TypeSet, 56 | Optional: true, 57 | MaxItems: 1, 58 | ConflictsWith: []string{"pull_triggers", "pull_trigger"}, 59 | Elem: &schema.Resource{ 60 | Schema: map[string]*schema.Schema{ 61 | "path": { 62 | Type: schema.TypeString, 63 | Description: "Context path", 64 | Required: true, 65 | ForceNew: true, 66 | }, 67 | "dockerfile": { 68 | Type: schema.TypeString, 69 | Description: "Name of the Dockerfile (Default is 'PATH/Dockerfile')", 70 | Optional: true, 71 | Default: "Dockerfile", 72 | ForceNew: true, 73 | }, 74 | "tag": { 75 | Type: schema.TypeList, 76 | Description: "Name and optionally a tag in the 'name:tag' format", 77 | Optional: true, 78 | Elem: &schema.Schema{ 79 | Type: schema.TypeString, 80 | }, 81 | }, 82 | "force_remove": { 83 | Type: schema.TypeBool, 84 | Description: "Always remove intermediate containers", 85 | Optional: true, 86 | }, 87 | "remove": { 88 | Type: schema.TypeBool, 89 | Description: "Remove intermediate containers after a successful build (default true)", 90 | Default: true, 91 | Optional: true, 92 | }, 93 | "no_cache": { 94 | Type: schema.TypeBool, 95 | Description: "Do not use cache when building the image", 96 | Optional: true, 97 | }, 98 | "target": { 99 | Type: schema.TypeString, 100 | Description: "Set the target build stage to build", 101 | Optional: true, 102 | }, 103 | "build_arg": { 104 | Type: schema.TypeMap, 105 | Description: "Set build-time variables", 106 | Optional: true, 107 | Elem: &schema.Schema{ 108 | Type: schema.TypeString, 109 | }, 110 | ForceNew: true, 111 | }, 112 | "label": { 113 | Type: schema.TypeMap, 114 | Description: "Set metadata for an image", 115 | Optional: true, 116 | Elem: &schema.Schema{ 117 | Type: schema.TypeString, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /docker/resource_docker_image_funcs.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "strings" 9 | 10 | "bytes" 11 | "encoding/base64" 12 | "encoding/json" 13 | 14 | "github.com/docker/cli/cli/command/image/build" 15 | "github.com/docker/docker/api/types" 16 | "github.com/docker/docker/client" 17 | "github.com/docker/docker/pkg/archive" 18 | "github.com/docker/docker/pkg/jsonmessage" 19 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 20 | "github.com/mitchellh/go-homedir" 21 | ) 22 | 23 | func getBuildContext(filePath string, excludes []string) io.Reader { 24 | filePath, _ = homedir.Expand(filePath) 25 | ctx, _ := archive.TarWithOptions(filePath, &archive.TarOptions{ 26 | ExcludePatterns: excludes, 27 | }) 28 | return ctx 29 | } 30 | 31 | func decodeBuildMessages(response types.ImageBuildResponse) (string, error) { 32 | buf := new(bytes.Buffer) 33 | buildErr := error(nil) 34 | 35 | dec := json.NewDecoder(response.Body) 36 | for dec.More() { 37 | var m jsonmessage.JSONMessage 38 | err := dec.Decode(&m) 39 | if err != nil { 40 | return buf.String(), fmt.Errorf("Problem decoding message from docker daemon: %s", err) 41 | } 42 | 43 | m.Display(buf, false) 44 | 45 | if m.Error != nil { 46 | buildErr = fmt.Errorf("Unable to build image") 47 | } 48 | } 49 | log.Printf("[DEBUG] %s", buf.String()) 50 | 51 | return buf.String(), buildErr 52 | } 53 | 54 | func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error { 55 | client := meta.(*ProviderConfig).DockerClient 56 | imageName := d.Get("name").(string) 57 | 58 | if value, ok := d.GetOk("build"); ok { 59 | for _, rawBuild := range value.(*schema.Set).List() { 60 | rawBuild := rawBuild.(map[string]interface{}) 61 | 62 | err := buildDockerImage(rawBuild, imageName, client) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | } 68 | apiImage, err := findImage(imageName, client, meta.(*ProviderConfig).AuthConfigs) 69 | if err != nil { 70 | return fmt.Errorf("Unable to read Docker image into resource: %s", err) 71 | } 72 | 73 | d.SetId(apiImage.ID + d.Get("name").(string)) 74 | return resourceDockerImageRead(d, meta) 75 | } 76 | 77 | func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error { 78 | client := meta.(*ProviderConfig).DockerClient 79 | var data Data 80 | if err := fetchLocalImages(&data, client); err != nil { 81 | return fmt.Errorf("Error reading docker image list: %s", err) 82 | } 83 | for id := range data.DockerImages { 84 | log.Printf("[DEBUG] local images data: %v", id) 85 | } 86 | foundImage := searchLocalImages(data, d.Get("name").(string)) 87 | 88 | if foundImage == nil { 89 | d.SetId("") 90 | return nil 91 | } 92 | 93 | d.SetId(foundImage.ID + d.Get("name").(string)) 94 | d.Set("latest", foundImage.ID) 95 | return nil 96 | } 97 | 98 | func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error { 99 | // We need to re-read in case switching parameters affects 100 | // the value of "latest" or others 101 | client := meta.(*ProviderConfig).DockerClient 102 | imageName := d.Get("name").(string) 103 | apiImage, err := findImage(imageName, client, meta.(*ProviderConfig).AuthConfigs) 104 | if err != nil { 105 | return fmt.Errorf("Unable to read Docker image into resource: %s", err) 106 | } 107 | 108 | d.Set("latest", apiImage.ID) 109 | 110 | return resourceDockerImageRead(d, meta) 111 | } 112 | 113 | func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error { 114 | client := meta.(*ProviderConfig).DockerClient 115 | err := removeImage(d, client) 116 | if err != nil { 117 | return fmt.Errorf("Unable to remove Docker image: %s", err) 118 | } 119 | d.SetId("") 120 | return nil 121 | } 122 | 123 | func searchLocalImages(data Data, imageName string) *types.ImageSummary { 124 | if apiImage, ok := data.DockerImages[imageName]; ok { 125 | log.Printf("[DEBUG] found local image via imageName: %v", imageName) 126 | return apiImage 127 | } 128 | if apiImage, ok := data.DockerImages[imageName+":latest"]; ok { 129 | log.Printf("[DEBUG] found local image via imageName + latest: %v", imageName) 130 | imageName = imageName + ":latest" 131 | return apiImage 132 | } 133 | return nil 134 | } 135 | 136 | func removeImage(d *schema.ResourceData, client *client.Client) error { 137 | var data Data 138 | 139 | if keepLocally := d.Get("keep_locally").(bool); keepLocally { 140 | return nil 141 | } 142 | 143 | if err := fetchLocalImages(&data, client); err != nil { 144 | return err 145 | } 146 | 147 | imageName := d.Get("name").(string) 148 | if imageName == "" { 149 | return fmt.Errorf("Empty image name is not allowed") 150 | } 151 | 152 | foundImage := searchLocalImages(data, imageName) 153 | 154 | if foundImage != nil { 155 | imageDeleteResponseItems, err := client.ImageRemove(context.Background(), foundImage.ID, types.ImageRemoveOptions{}) 156 | if err != nil { 157 | return err 158 | } 159 | log.Printf("[INFO] Deleted image items: %v", imageDeleteResponseItems) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | func fetchLocalImages(data *Data, client *client.Client) error { 166 | images, err := client.ImageList(context.Background(), types.ImageListOptions{All: false}) 167 | if err != nil { 168 | return fmt.Errorf("Unable to list Docker images: %s", err) 169 | } 170 | 171 | if data.DockerImages == nil { 172 | data.DockerImages = make(map[string]*types.ImageSummary) 173 | } 174 | 175 | // Docker uses different nomenclatures in different places...sometimes a short 176 | // ID, sometimes long, etc. So we store both in the map so we can always find 177 | // the same image object. We store the tags and digests, too. 178 | for i, image := range images { 179 | data.DockerImages[image.ID[:12]] = &images[i] 180 | data.DockerImages[image.ID] = &images[i] 181 | for _, repotag := range image.RepoTags { 182 | data.DockerImages[repotag] = &images[i] 183 | } 184 | for _, repodigest := range image.RepoDigests { 185 | data.DockerImages[repodigest] = &images[i] 186 | } 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func pullImage(data *Data, client *client.Client, authConfig *AuthConfigs, image string) error { 193 | pullOpts := parseImageOptions(image) 194 | 195 | // If a registry was specified in the image name, try to find auth for it 196 | auth := types.AuthConfig{} 197 | if pullOpts.Registry != "" { 198 | if authConfig, ok := authConfig.Configs[normalizeRegistryAddress(pullOpts.Registry)]; ok { 199 | auth = authConfig 200 | } 201 | } else { 202 | // Try to find an auth config for the public docker hub if a registry wasn't given 203 | if authConfig, ok := authConfig.Configs["https://registry.hub.docker.com"]; ok { 204 | auth = authConfig 205 | } 206 | } 207 | 208 | encodedJSON, err := json.Marshal(auth) 209 | if err != nil { 210 | return fmt.Errorf("error creating auth config: %s", err) 211 | } 212 | 213 | out, err := client.ImagePull(context.Background(), image, types.ImagePullOptions{ 214 | RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON), 215 | }) 216 | if err != nil { 217 | return fmt.Errorf("error pulling image %s: %s", image, err) 218 | } 219 | defer out.Close() 220 | 221 | buf := new(bytes.Buffer) 222 | buf.ReadFrom(out) 223 | s := buf.String() 224 | log.Printf("[DEBUG] pulled image %v: %v", image, s) 225 | 226 | return nil 227 | } 228 | 229 | type internalPullImageOptions struct { 230 | Repository string `qs:"fromImage"` 231 | Tag string 232 | 233 | // Only required for Docker Engine 1.9 or 1.10 w/ Remote API < 1.21 234 | // and Docker Engine < 1.9 235 | // This parameter was removed in Docker Engine 1.11 236 | Registry string 237 | } 238 | 239 | func parseImageOptions(image string) internalPullImageOptions { 240 | pullOpts := internalPullImageOptions{} 241 | 242 | // Pre-fill with image by default, update later if tag found 243 | pullOpts.Repository = image 244 | 245 | firstSlash := strings.Index(image, "/") 246 | 247 | // Detect the registry name - it should either contain port, be fully qualified or be localhost 248 | // If the image contains more than 2 path components, or at least one and the prefix looks like a hostname 249 | if strings.Count(image, "/") > 1 || firstSlash != -1 && (strings.ContainsAny(image[:firstSlash], ".:") || image[:firstSlash] == "localhost") { 250 | // registry/repo/image 251 | pullOpts.Registry = image[:firstSlash] 252 | } 253 | 254 | prefixLength := len(pullOpts.Registry) 255 | tagIndex := strings.Index(image[prefixLength:], ":") 256 | 257 | if tagIndex != -1 { 258 | // we have the tag, strip it 259 | pullOpts.Repository = image[:prefixLength+tagIndex] 260 | pullOpts.Tag = image[prefixLength+tagIndex+1:] 261 | } 262 | 263 | return pullOpts 264 | } 265 | 266 | func findImage(imageName string, client *client.Client, authConfig *AuthConfigs) (*types.ImageSummary, error) { 267 | if imageName == "" { 268 | return nil, fmt.Errorf("Empty image name is not allowed") 269 | } 270 | 271 | var data Data 272 | // load local images into the data structure 273 | if err := fetchLocalImages(&data, client); err != nil { 274 | return nil, err 275 | } 276 | 277 | foundImage := searchLocalImages(data, imageName) 278 | if foundImage != nil { 279 | return foundImage, nil 280 | } 281 | 282 | if err := pullImage(&data, client, authConfig, imageName); err != nil { 283 | return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err) 284 | } 285 | 286 | // update the data structure of the images 287 | if err := fetchLocalImages(&data, client); err != nil { 288 | return nil, err 289 | } 290 | 291 | foundImage = searchLocalImages(data, imageName) 292 | if foundImage != nil { 293 | return foundImage, nil 294 | } 295 | 296 | return nil, fmt.Errorf("Unable to find or pull image %s", imageName) 297 | } 298 | 299 | func buildDockerImage(rawBuild map[string]interface{}, imageName string, client *client.Client) error { 300 | buildOptions := types.ImageBuildOptions{} 301 | 302 | buildOptions.Version = types.BuilderV1 303 | buildOptions.Dockerfile = rawBuild["dockerfile"].(string) 304 | 305 | tags := []string{imageName} 306 | for _, t := range rawBuild["tag"].([]interface{}) { 307 | tags = append(tags, t.(string)) 308 | } 309 | buildOptions.Tags = tags 310 | 311 | buildOptions.ForceRemove = rawBuild["force_remove"].(bool) 312 | buildOptions.Remove = rawBuild["remove"].(bool) 313 | buildOptions.NoCache = rawBuild["no_cache"].(bool) 314 | buildOptions.Target = rawBuild["target"].(string) 315 | 316 | buildArgs := make(map[string]*string) 317 | for k, v := range rawBuild["build_arg"].(map[string]interface{}) { 318 | val := v.(string) 319 | buildArgs[k] = &val 320 | } 321 | buildOptions.BuildArgs = buildArgs 322 | log.Printf("[DEBUG] Build Args: %v\n", buildArgs) 323 | 324 | labels := make(map[string]string) 325 | for k, v := range rawBuild["label"].(map[string]interface{}) { 326 | labels[k] = v.(string) 327 | } 328 | buildOptions.Labels = labels 329 | log.Printf("[DEBUG] Labels: %v\n", labels) 330 | 331 | contextDir := rawBuild["path"].(string) 332 | excludes, err := build.ReadDockerignore(contextDir) 333 | if err != nil { 334 | return err 335 | } 336 | excludes = build.TrimBuildFilesFromExcludes(excludes, buildOptions.Dockerfile, false) 337 | 338 | var response types.ImageBuildResponse 339 | response, err = client.ImageBuild(context.Background(), getBuildContext(contextDir, excludes), buildOptions) 340 | if err != nil { 341 | return err 342 | } 343 | defer response.Body.Close() 344 | 345 | buildResult, err := decodeBuildMessages(response) 346 | if err != nil { 347 | return fmt.Errorf("%s\n\n%s", err, buildResult) 348 | } 349 | return nil 350 | } 351 | -------------------------------------------------------------------------------- /docker/resource_docker_image_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "regexp" 10 | "testing" 11 | 12 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 13 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 14 | ) 15 | 16 | var contentDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`) 17 | 18 | func TestAccDockerImage_basic(t *testing.T) { 19 | resource.Test(t, resource.TestCase{ 20 | PreCheck: func() { testAccPreCheck(t) }, 21 | Providers: testAccProviders, 22 | CheckDestroy: testAccDockerImageDestroy, 23 | Steps: []resource.TestStep{ 24 | { 25 | Config: testAccDockerImageConfig, 26 | Check: resource.ComposeTestCheckFunc( 27 | resource.TestMatchResourceAttr("docker_image.foo", "latest", contentDigestRegexp), 28 | ), 29 | }, 30 | }, 31 | }) 32 | } 33 | 34 | func TestAccDockerImage_private(t *testing.T) { 35 | resource.Test(t, resource.TestCase{ 36 | PreCheck: func() { testAccPreCheck(t) }, 37 | Providers: testAccProviders, 38 | CheckDestroy: testAccDockerImageDestroy, 39 | Steps: []resource.TestStep{ 40 | { 41 | Config: testAddDockerPrivateImageConfig, 42 | Check: resource.ComposeTestCheckFunc( 43 | resource.TestMatchResourceAttr("docker_image.foobar", "latest", contentDigestRegexp), 44 | ), 45 | }, 46 | }, 47 | }) 48 | } 49 | 50 | func TestAccDockerImage_destroy(t *testing.T) { 51 | resource.Test(t, resource.TestCase{ 52 | PreCheck: func() { testAccPreCheck(t) }, 53 | Providers: testAccProviders, 54 | CheckDestroy: func(s *terraform.State) error { 55 | for _, rs := range s.RootModule().Resources { 56 | if rs.Type != "docker_image" { 57 | continue 58 | } 59 | 60 | client := testAccProvider.Meta().(*ProviderConfig).DockerClient 61 | _, _, err := client.ImageInspectWithRaw(context.Background(), rs.Primary.Attributes["latest"]) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | }, 68 | Steps: []resource.TestStep{ 69 | { 70 | Config: testAccDockerImageKeepLocallyConfig, 71 | Check: resource.ComposeTestCheckFunc( 72 | resource.TestMatchResourceAttr("docker_image.foobarzoo", "latest", contentDigestRegexp), 73 | ), 74 | }, 75 | }, 76 | }) 77 | } 78 | 79 | func TestAccDockerImage_data(t *testing.T) { 80 | resource.Test(t, resource.TestCase{ 81 | PreCheck: func() { testAccPreCheck(t) }, 82 | Providers: testAccProviders, 83 | PreventPostDestroyRefresh: true, 84 | Steps: []resource.TestStep{ 85 | { 86 | Config: testAccDockerImageFromDataConfig, 87 | Check: resource.ComposeTestCheckFunc( 88 | resource.TestMatchResourceAttr("docker_image.foobarbaz", "latest", contentDigestRegexp), 89 | ), 90 | }, 91 | }, 92 | }) 93 | } 94 | 95 | func TestAccDockerImage_data_pull_trigger(t *testing.T) { 96 | resource.Test(t, resource.TestCase{ 97 | PreCheck: func() { testAccPreCheck(t) }, 98 | Providers: testAccProviders, 99 | PreventPostDestroyRefresh: true, 100 | Steps: []resource.TestStep{ 101 | { 102 | Config: testAccDockerImageFromDataConfigWithPullTrigger, 103 | Check: resource.ComposeTestCheckFunc( 104 | resource.TestMatchResourceAttr("docker_image.foobarbazoo", "latest", contentDigestRegexp), 105 | ), 106 | }, 107 | }, 108 | }) 109 | } 110 | 111 | func TestAccDockerImage_data_private(t *testing.T) { 112 | registry := "127.0.0.1:15000" 113 | image := "127.0.0.1:15000/tftest-service:v1" 114 | 115 | resource.Test(t, resource.TestCase{ 116 | PreCheck: func() { testAccPreCheck(t) }, 117 | Providers: testAccProviders, 118 | PreventPostDestroyRefresh: true, 119 | Steps: []resource.TestStep{ 120 | { 121 | Config: fmt.Sprintf(testAccDockerImageFromDataPrivateConfig, registry, image), 122 | Check: resource.ComposeTestCheckFunc( 123 | resource.TestMatchResourceAttr("docker_image.foo_private", "latest", contentDigestRegexp), 124 | ), 125 | }, 126 | }, 127 | CheckDestroy: checkAndRemoveImages, 128 | }) 129 | } 130 | 131 | func TestAccDockerImage_data_private_config_file(t *testing.T) { 132 | registry := "127.0.0.1:15000" 133 | image := "127.0.0.1:15000/tftest-service:v1" 134 | wd, _ := os.Getwd() 135 | dockerConfig := wd + "/../scripts/testing/dockerconfig.json" 136 | 137 | resource.Test(t, resource.TestCase{ 138 | PreCheck: func() { testAccPreCheck(t) }, 139 | Providers: testAccProviders, 140 | PreventPostDestroyRefresh: true, 141 | Steps: []resource.TestStep{ 142 | { 143 | Config: fmt.Sprintf(testAccDockerImageFromDataPrivateConfigFile, registry, dockerConfig, image), 144 | Check: resource.ComposeTestCheckFunc( 145 | resource.TestMatchResourceAttr("docker_image.foo_private", "latest", contentDigestRegexp), 146 | ), 147 | }, 148 | }, 149 | CheckDestroy: checkAndRemoveImages, 150 | }) 151 | } 152 | 153 | func TestAccDockerImage_data_private_config_file_content(t *testing.T) { 154 | registry := "127.0.0.1:15000" 155 | image := "127.0.0.1:15000/tftest-service:v1" 156 | wd, _ := os.Getwd() 157 | dockerConfig := wd + "/../scripts/testing/dockerconfig.json" 158 | 159 | resource.Test(t, resource.TestCase{ 160 | PreCheck: func() { testAccPreCheck(t) }, 161 | Providers: testAccProviders, 162 | PreventPostDestroyRefresh: true, 163 | Steps: []resource.TestStep{ 164 | { 165 | Config: fmt.Sprintf(testAccDockerImageFromDataPrivateConfigFileContent, registry, dockerConfig, image), 166 | Check: resource.ComposeTestCheckFunc( 167 | resource.TestMatchResourceAttr("docker_image.foo_private", "latest", contentDigestRegexp), 168 | ), 169 | }, 170 | }, 171 | CheckDestroy: checkAndRemoveImages, 172 | }) 173 | } 174 | 175 | func TestAccDockerImage_sha265(t *testing.T) { 176 | resource.Test(t, resource.TestCase{ 177 | PreCheck: func() { testAccPreCheck(t) }, 178 | Providers: testAccProviders, 179 | CheckDestroy: testAccDockerImageDestroy, 180 | Steps: []resource.TestStep{ 181 | { 182 | Config: testAddDockerImageWithSHA256RepoDigest, 183 | Check: resource.ComposeTestCheckFunc( 184 | resource.TestMatchResourceAttr("docker_image.foobar", "latest", contentDigestRegexp), 185 | ), 186 | }, 187 | }, 188 | }) 189 | } 190 | 191 | func testAccDockerImageDestroy(s *terraform.State) error { 192 | for _, rs := range s.RootModule().Resources { 193 | if rs.Type != "docker_image" { 194 | continue 195 | } 196 | 197 | client := testAccProvider.Meta().(*ProviderConfig).DockerClient 198 | _, _, err := client.ImageInspectWithRaw(context.Background(), rs.Primary.Attributes["latest"]) 199 | if err == nil { 200 | return fmt.Errorf("Image still exists") 201 | } 202 | } 203 | return nil 204 | } 205 | 206 | func TestAccDockerImage_build(t *testing.T) { 207 | wd, _ := os.Getwd() 208 | dfPath := path.Join(wd, "Dockerfile") 209 | ioutil.WriteFile(dfPath, []byte(testDockerFileExample), 0644) 210 | defer os.Remove(dfPath) 211 | resource.Test(t, resource.TestCase{ 212 | PreCheck: func() { testAccPreCheck(t) }, 213 | Providers: testAccProviders, 214 | CheckDestroy: testAccDockerImageDestroy, 215 | Steps: []resource.TestStep{ 216 | { 217 | Config: testCreateDockerImage, 218 | Check: resource.ComposeTestCheckFunc( 219 | resource.TestMatchResourceAttr("docker_image.test", "name", contentDigestRegexp), 220 | ), 221 | }, 222 | }, 223 | }) 224 | } 225 | 226 | const testAccDockerImageConfig = ` 227 | resource "docker_image" "foo" { 228 | name = "alpine:3.1" 229 | } 230 | ` 231 | 232 | const testAddDockerPrivateImageConfig = ` 233 | resource "docker_image" "foobar" { 234 | name = "gcr.io:443/google_containers/pause:0.8.0" 235 | } 236 | ` 237 | 238 | const testAccDockerImageKeepLocallyConfig = ` 239 | resource "docker_image" "foobarzoo" { 240 | name = "crux:3.1" 241 | keep_locally = true 242 | } 243 | ` 244 | 245 | const testAccDockerImageFromDataConfig = ` 246 | data "docker_registry_image" "foobarbaz" { 247 | name = "alpine:3.1" 248 | } 249 | resource "docker_image" "foobarbaz" { 250 | name = "${data.docker_registry_image.foobarbaz.name}" 251 | pull_triggers = ["${data.docker_registry_image.foobarbaz.sha256_digest}"] 252 | } 253 | ` 254 | 255 | const testAccDockerImageFromDataConfigWithPullTrigger = ` 256 | data "docker_registry_image" "foobarbazoo" { 257 | name = "alpine:3.1" 258 | } 259 | resource "docker_image" "foobarbazoo" { 260 | name = "${data.docker_registry_image.foobarbazoo.name}" 261 | pull_trigger = "${data.docker_registry_image.foobarbazoo.sha256_digest}" 262 | } 263 | ` 264 | 265 | const testAccDockerImageFromDataPrivateConfig = ` 266 | provider "docker" { 267 | alias = "private" 268 | registry_auth { 269 | address = "%s" 270 | } 271 | } 272 | data "docker_registry_image" "foo_private" { 273 | provider = "docker.private" 274 | name = "%s" 275 | } 276 | resource "docker_image" "foo_private" { 277 | provider = "docker.private" 278 | name = "${data.docker_registry_image.foo_private.name}" 279 | keep_locally = true 280 | pull_triggers = ["${data.docker_registry_image.foo_private.sha256_digest}"] 281 | } 282 | ` 283 | 284 | const testAccDockerImageFromDataPrivateConfigFile = ` 285 | provider "docker" { 286 | alias = "private" 287 | registry_auth { 288 | address = "%s" 289 | config_file = "%s" 290 | } 291 | } 292 | resource "docker_image" "foo_private" { 293 | provider = "docker.private" 294 | name = "%s" 295 | } 296 | ` 297 | 298 | const testAccDockerImageFromDataPrivateConfigFileContent = ` 299 | provider "docker" { 300 | alias = "private" 301 | registry_auth { 302 | address = "%s" 303 | config_file_content = "${file("%s")}" 304 | } 305 | } 306 | resource "docker_image" "foo_private" { 307 | provider = "docker.private" 308 | name = "%s" 309 | } 310 | ` 311 | 312 | const testAddDockerImageWithSHA256RepoDigest = ` 313 | resource "docker_image" "foobar" { 314 | name = "stocard/gotthard@sha256:ed752380c07940c651b46c97ca2101034b3be112f4d86198900aa6141f37fe7b" 315 | } 316 | ` 317 | 318 | const testCreateDockerImage = ` 319 | resource "docker_image" "test" { 320 | name = "ubuntu:11" 321 | build { 322 | path = "." 323 | dockerfile = "Dockerfile" 324 | force_remove = true 325 | build_arg = { 326 | test_arg = "kenobi" 327 | } 328 | label = { 329 | test_label1 = "han" 330 | test_label2 = "solo" 331 | } 332 | } 333 | } 334 | ` 335 | 336 | const testDockerFileExample = ` 337 | FROM python:3-stretch 338 | 339 | WORKDIR /app 340 | 341 | ARG test_arg 342 | 343 | RUN echo ${test_arg} > test_arg.txt 344 | 345 | RUN apt-get update -qq 346 | ` 347 | -------------------------------------------------------------------------------- /docker/resource_docker_network.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "regexp" 7 | "strconv" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 10 | ) 11 | 12 | func resourceDockerNetwork() *schema.Resource { 13 | return &schema.Resource{ 14 | Create: resourceDockerNetworkCreate, 15 | Read: resourceDockerNetworkRead, 16 | Delete: resourceDockerNetworkDelete, 17 | Importer: &schema.ResourceImporter{ 18 | State: schema.ImportStatePassthrough, 19 | }, 20 | 21 | Schema: map[string]*schema.Schema{ 22 | "name": { 23 | Type: schema.TypeString, 24 | Required: true, 25 | ForceNew: true, 26 | }, 27 | 28 | "labels": { 29 | Type: schema.TypeSet, 30 | Optional: true, 31 | ForceNew: true, 32 | Elem: labelSchema, 33 | }, 34 | 35 | "check_duplicate": { 36 | Type: schema.TypeBool, 37 | Optional: true, 38 | ForceNew: true, 39 | }, 40 | 41 | "driver": { 42 | Type: schema.TypeString, 43 | Optional: true, 44 | ForceNew: true, 45 | Computed: true, 46 | }, 47 | 48 | "options": { 49 | Type: schema.TypeMap, 50 | Optional: true, 51 | ForceNew: true, 52 | Computed: true, 53 | }, 54 | 55 | "internal": { 56 | Type: schema.TypeBool, 57 | Optional: true, 58 | Computed: true, 59 | ForceNew: true, 60 | }, 61 | 62 | "attachable": { 63 | Type: schema.TypeBool, 64 | Optional: true, 65 | ForceNew: true, 66 | }, 67 | 68 | "ingress": { 69 | Type: schema.TypeBool, 70 | Optional: true, 71 | ForceNew: true, 72 | }, 73 | 74 | "ipv6": { 75 | Type: schema.TypeBool, 76 | Optional: true, 77 | ForceNew: true, 78 | }, 79 | 80 | "ipam_driver": { 81 | Type: schema.TypeString, 82 | Optional: true, 83 | ForceNew: true, 84 | Default: "default", 85 | }, 86 | 87 | "ipam_config": { 88 | Type: schema.TypeSet, 89 | Optional: true, 90 | Computed: true, 91 | ForceNew: true, 92 | // DiffSuppressFunc: suppressIfIPAMConfigWithIpv6Changes(), 93 | Elem: &schema.Resource{ 94 | Schema: map[string]*schema.Schema{ 95 | "subnet": { 96 | Type: schema.TypeString, 97 | Optional: true, 98 | ForceNew: true, 99 | }, 100 | 101 | "ip_range": { 102 | Type: schema.TypeString, 103 | Optional: true, 104 | ForceNew: true, 105 | }, 106 | 107 | "gateway": { 108 | Type: schema.TypeString, 109 | Optional: true, 110 | ForceNew: true, 111 | }, 112 | 113 | "aux_address": { 114 | Type: schema.TypeMap, 115 | Optional: true, 116 | ForceNew: true, 117 | }, 118 | }, 119 | }, 120 | }, 121 | 122 | "scope": { 123 | Type: schema.TypeString, 124 | Computed: true, 125 | }, 126 | }, 127 | SchemaVersion: 1, 128 | StateUpgraders: []schema.StateUpgrader{ 129 | { 130 | Version: 0, 131 | Type: resourceDockerNetworkV0().CoreConfigSchema().ImpliedType(), 132 | Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { 133 | return replaceLabelsMapFieldWithSetField(rawState), nil 134 | }, 135 | }, 136 | }, 137 | } 138 | } 139 | 140 | func resourceDockerNetworkV0() *schema.Resource { 141 | return &schema.Resource{ 142 | //This is only used for state migration, so the CRUD 143 | //callbacks are no longer relevant 144 | Schema: map[string]*schema.Schema{ 145 | "name": { 146 | Type: schema.TypeString, 147 | Required: true, 148 | ForceNew: true, 149 | }, 150 | 151 | "labels": { 152 | Type: schema.TypeMap, 153 | Optional: true, 154 | ForceNew: true, 155 | }, 156 | 157 | "check_duplicate": { 158 | Type: schema.TypeBool, 159 | Optional: true, 160 | ForceNew: true, 161 | }, 162 | 163 | "driver": { 164 | Type: schema.TypeString, 165 | Optional: true, 166 | ForceNew: true, 167 | Default: "bridge", 168 | }, 169 | 170 | "options": { 171 | Type: schema.TypeMap, 172 | Optional: true, 173 | ForceNew: true, 174 | Computed: true, 175 | }, 176 | 177 | "internal": { 178 | Type: schema.TypeBool, 179 | Optional: true, 180 | Computed: true, 181 | ForceNew: true, 182 | }, 183 | 184 | "attachable": { 185 | Type: schema.TypeBool, 186 | Optional: true, 187 | ForceNew: true, 188 | }, 189 | 190 | "ingress": { 191 | Type: schema.TypeBool, 192 | Optional: true, 193 | ForceNew: true, 194 | }, 195 | 196 | "ipv6": { 197 | Type: schema.TypeBool, 198 | Optional: true, 199 | ForceNew: true, 200 | }, 201 | 202 | "ipam_driver": { 203 | Type: schema.TypeString, 204 | Optional: true, 205 | ForceNew: true, 206 | Default: "default", 207 | }, 208 | 209 | "ipam_config": { 210 | Type: schema.TypeSet, 211 | Optional: true, 212 | Computed: true, 213 | ForceNew: true, 214 | // DiffSuppressFunc: suppressIfIPAMConfigWithIpv6Changes(), 215 | Elem: &schema.Resource{ 216 | Schema: map[string]*schema.Schema{ 217 | "subnet": { 218 | Type: schema.TypeString, 219 | Optional: true, 220 | ForceNew: true, 221 | }, 222 | 223 | "ip_range": { 224 | Type: schema.TypeString, 225 | Optional: true, 226 | ForceNew: true, 227 | }, 228 | 229 | "gateway": { 230 | Type: schema.TypeString, 231 | Optional: true, 232 | ForceNew: true, 233 | }, 234 | 235 | "aux_address": { 236 | Type: schema.TypeMap, 237 | Optional: true, 238 | ForceNew: true, 239 | }, 240 | }, 241 | }, 242 | }, 243 | 244 | "scope": { 245 | Type: schema.TypeString, 246 | Computed: true, 247 | }, 248 | }, 249 | } 250 | } 251 | 252 | func suppressIfIPAMConfigWithIpv6Changes() schema.SchemaDiffSuppressFunc { 253 | return func(k, old, new string, d *schema.ResourceData) bool { 254 | // the initial case when the resource is created 255 | if old == "" && new != "" { 256 | return false 257 | } 258 | 259 | // if ipv6 is not given we do not consider 260 | ipv6, ok := d.GetOk("ipv6") 261 | if !ok { 262 | return false 263 | } 264 | 265 | // if ipv6 is given but false we do not consider 266 | isIPv6 := ipv6.(bool) 267 | if !isIPv6 { 268 | return false 269 | } 270 | if k == "ipam_config.#" { 271 | log.Printf("[INFO] ipam_config: k: %q, old: %s, new: %s\n", k, old, new) 272 | oldVal, _ := strconv.Atoi(string(old)) 273 | newVal, _ := strconv.Atoi(string(new)) 274 | log.Printf("[INFO] ipam_config: oldVal: %d, newVal: %d\n", oldVal, newVal) 275 | if newVal <= oldVal { 276 | log.Printf("[INFO] suppressingDiff for ipam_config: oldVal: %d, newVal: %d\n", oldVal, newVal) 277 | return true 278 | } 279 | } 280 | if regexp.MustCompile(`ipam_config\.\d+\.gateway`).MatchString(k) { 281 | ip := net.ParseIP(old) 282 | ipv4Address := ip.To4() 283 | log.Printf("[INFO] ipam_config.gateway: k: %q, old: %s, new: %s - %v\n", k, old, new, ipv4Address != nil) 284 | // is an ipv4Address and content changed from non-empty to empty 285 | if ipv4Address != nil && old != "" && new == "" { 286 | log.Printf("[INFO] suppressingDiff for ipam_config.gateway %q: oldVal: %s, newVal: %s\n", ipv4Address.String(), old, new) 287 | return true 288 | } 289 | } 290 | if regexp.MustCompile(`ipam_config\.\d+\.subnet`).MatchString(k) { 291 | if old != "" && new == "" { 292 | log.Printf("[INFO] suppressingDiff for ipam_config.subnet: oldVal: %s, newVal: %s\n", old, new) 293 | return true 294 | } 295 | } 296 | return false 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /docker/resource_docker_network_funcs.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "context" 8 | "encoding/json" 9 | "log" 10 | "time" 11 | 12 | "github.com/docker/docker/api/types" 13 | "github.com/docker/docker/api/types/network" 14 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 15 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 16 | ) 17 | 18 | func resourceDockerNetworkCreate(d *schema.ResourceData, meta interface{}) error { 19 | client := meta.(*ProviderConfig).DockerClient 20 | 21 | createOpts := types.NetworkCreate{} 22 | if v, ok := d.GetOk("labels"); ok { 23 | createOpts.Labels = labelSetToMap(v.(*schema.Set)) 24 | } 25 | if v, ok := d.GetOk("check_duplicate"); ok { 26 | createOpts.CheckDuplicate = v.(bool) 27 | } 28 | if v, ok := d.GetOk("driver"); ok { 29 | createOpts.Driver = v.(string) 30 | } 31 | if v, ok := d.GetOk("options"); ok { 32 | createOpts.Options = mapTypeMapValsToString(v.(map[string]interface{})) 33 | } 34 | if v, ok := d.GetOk("internal"); ok { 35 | createOpts.Internal = v.(bool) 36 | } 37 | if v, ok := d.GetOk("attachable"); ok { 38 | createOpts.Attachable = v.(bool) 39 | } 40 | if v, ok := d.GetOk("ingress"); ok { 41 | createOpts.Ingress = v.(bool) 42 | } 43 | if v, ok := d.GetOk("ipv6"); ok { 44 | createOpts.EnableIPv6 = v.(bool) 45 | } 46 | 47 | ipamOpts := &network.IPAM{} 48 | ipamOptsSet := false 49 | if v, ok := d.GetOk("ipam_driver"); ok { 50 | ipamOpts.Driver = v.(string) 51 | ipamOptsSet = true 52 | } 53 | if v, ok := d.GetOk("ipam_config"); ok { 54 | ipamOpts.Config = ipamConfigSetToIpamConfigs(v.(*schema.Set)) 55 | ipamOptsSet = true 56 | } 57 | 58 | if ipamOptsSet { 59 | createOpts.IPAM = ipamOpts 60 | } 61 | 62 | retNetwork := types.NetworkCreateResponse{} 63 | retNetwork, err := client.NetworkCreate(context.Background(), d.Get("name").(string), createOpts) 64 | if err != nil { 65 | return fmt.Errorf("Unable to create network: %s", err) 66 | } 67 | 68 | d.SetId(retNetwork.ID) 69 | // d.Set("check_duplicate") TODO 70 | return resourceDockerNetworkRead(d, meta) 71 | } 72 | 73 | func resourceDockerNetworkRead(d *schema.ResourceData, meta interface{}) error { 74 | log.Printf("[INFO] Waiting for network: '%s' to expose all fields: max '%v seconds'", d.Id(), 30) 75 | 76 | stateConf := &resource.StateChangeConf{ 77 | Pending: []string{"pending"}, 78 | Target: []string{"all_fields", "removed"}, 79 | Refresh: resourceDockerNetworkReadRefreshFunc(d, meta), 80 | Timeout: 30 * time.Second, 81 | MinTimeout: 5 * time.Second, 82 | Delay: 2 * time.Second, 83 | } 84 | 85 | // Wait, catching any errors 86 | _, err := stateConf.WaitForState() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func resourceDockerNetworkDelete(d *schema.ResourceData, meta interface{}) error { 95 | log.Printf("[INFO] Waiting for network: '%s' to be removed: max '%v seconds'", d.Id(), 30) 96 | 97 | stateConf := &resource.StateChangeConf{ 98 | Pending: []string{"pending"}, 99 | Target: []string{"removed"}, 100 | Refresh: resourceDockerNetworkRemoveRefreshFunc(d, meta), 101 | Timeout: 30 * time.Second, 102 | MinTimeout: 5 * time.Second, 103 | Delay: 2 * time.Second, 104 | } 105 | 106 | // Wait, catching any errors 107 | _, err := stateConf.WaitForState() 108 | if err != nil { 109 | return err 110 | } 111 | 112 | d.SetId("") 113 | return nil 114 | } 115 | 116 | func ipamConfigSetToIpamConfigs(ipamConfigSet *schema.Set) []network.IPAMConfig { 117 | ipamConfigs := make([]network.IPAMConfig, ipamConfigSet.Len()) 118 | 119 | for i, ipamConfigInt := range ipamConfigSet.List() { 120 | ipamConfigRaw := ipamConfigInt.(map[string]interface{}) 121 | 122 | ipamConfig := network.IPAMConfig{} 123 | ipamConfig.Subnet = ipamConfigRaw["subnet"].(string) 124 | ipamConfig.IPRange = ipamConfigRaw["ip_range"].(string) 125 | ipamConfig.Gateway = ipamConfigRaw["gateway"].(string) 126 | 127 | auxAddressRaw := ipamConfigRaw["aux_address"].(map[string]interface{}) 128 | ipamConfig.AuxAddress = make(map[string]string, len(auxAddressRaw)) 129 | for k, v := range auxAddressRaw { 130 | ipamConfig.AuxAddress[k] = v.(string) 131 | } 132 | 133 | ipamConfigs[i] = ipamConfig 134 | } 135 | 136 | return ipamConfigs 137 | } 138 | 139 | func resourceDockerNetworkReadRefreshFunc( 140 | d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 141 | return func() (interface{}, string, error) { 142 | client := meta.(*ProviderConfig).DockerClient 143 | networkID := d.Id() 144 | 145 | retNetwork, _, err := client.NetworkInspectWithRaw(context.Background(), networkID, types.NetworkInspectOptions{}) 146 | if err != nil { 147 | log.Printf("[WARN] Network (%s) not found, removing from state", networkID) 148 | d.SetId("") 149 | return networkID, "removed", nil 150 | } 151 | 152 | jsonObj, _ := json.MarshalIndent(retNetwork, "", "\t") 153 | log.Printf("[DEBUG] Docker network inspect: %s", jsonObj) 154 | 155 | d.Set("name", retNetwork.Name) 156 | d.Set("labels", mapToLabelSet(retNetwork.Labels)) 157 | d.Set("driver", retNetwork.Driver) 158 | d.Set("internal", retNetwork.Internal) 159 | d.Set("attachable", retNetwork.Attachable) 160 | d.Set("ingress", retNetwork.Ingress) 161 | d.Set("ipv6", retNetwork.EnableIPv6) 162 | d.Set("ipam_driver", retNetwork.IPAM.Driver) 163 | d.Set("scope", retNetwork.Scope) 164 | if retNetwork.Scope == "overlay" { 165 | if retNetwork.Options != nil && len(retNetwork.Options) != 0 { 166 | d.Set("options", retNetwork.Options) 167 | } else { 168 | log.Printf("[DEBUG] options: %v not exposed", retNetwork.Options) 169 | return networkID, "pending", nil 170 | } 171 | } else { 172 | d.Set("options", retNetwork.Options) 173 | } 174 | 175 | if err = d.Set("ipam_config", flattenIpamConfigSpec(retNetwork.IPAM.Config)); err != nil { 176 | log.Printf("[WARN] failed to set ipam config from API: %s", err) 177 | } 178 | 179 | log.Println("[DEBUG] all network fields exposed") 180 | return networkID, "all_fields", nil 181 | } 182 | } 183 | 184 | func resourceDockerNetworkRemoveRefreshFunc( 185 | d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 186 | return func() (interface{}, string, error) { 187 | client := meta.(*ProviderConfig).DockerClient 188 | networkID := d.Id() 189 | 190 | _, _, err := client.NetworkInspectWithRaw(context.Background(), networkID, types.NetworkInspectOptions{}) 191 | if err != nil { 192 | log.Printf("[INFO] Network (%s) not found. Already removed", networkID) 193 | return networkID, "removed", nil 194 | } 195 | 196 | if err := client.NetworkRemove(context.Background(), networkID); err != nil { 197 | if strings.Contains(err.Error(), "has active endpoints") { 198 | return networkID, "pending", nil 199 | } 200 | return networkID, "other", err 201 | } 202 | 203 | return networkID, "removed", nil 204 | } 205 | } 206 | 207 | // TODO mavogel: separate structure file 208 | // TODO 2: seems like we can replace the set hash generation with plain lists -> #219 209 | func flattenIpamConfigSpec(in []network.IPAMConfig) *schema.Set { // []interface{} { 210 | var out = make([]interface{}, len(in), len(in)) 211 | for i, v := range in { 212 | log.Printf("[DEBUG] flatten ipam %d: %#v", i, v) 213 | m := make(map[string]interface{}) 214 | if len(v.Subnet) > 0 { 215 | m["subnet"] = v.Subnet 216 | } 217 | if len(v.IPRange) > 0 { 218 | m["ip_range"] = v.IPRange 219 | } 220 | if len(v.Gateway) > 0 { 221 | m["gateway"] = v.Gateway 222 | } 223 | if len(v.AuxAddress) > 0 { 224 | m["aux_address"] = v.AuxAddress 225 | } 226 | out[i] = m 227 | } 228 | // log.Printf("[INFO] flatten ipam out: %#v", out) 229 | imapConfigsResource := resourceDockerNetwork().Schema["ipam_config"].Elem.(*schema.Resource) 230 | f := schema.HashResource(imapConfigsResource) 231 | return schema.NewSet(f, out) 232 | // return out 233 | } 234 | -------------------------------------------------------------------------------- /docker/resource_docker_network_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "context" 8 | 9 | "github.com/docker/docker/api/types" 10 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 11 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 12 | ) 13 | 14 | func TestAccDockerNetwork_basic(t *testing.T) { 15 | var n types.NetworkResource 16 | resourceName := "docker_network.foo" 17 | 18 | resource.Test(t, resource.TestCase{ 19 | PreCheck: func() { testAccPreCheck(t) }, 20 | Providers: testAccProviders, 21 | Steps: []resource.TestStep{ 22 | { 23 | Config: testAccDockerNetworkConfig, 24 | Check: resource.ComposeTestCheckFunc( 25 | testAccNetwork(resourceName, &n), 26 | ), 27 | }, 28 | { 29 | ResourceName: resourceName, 30 | ImportState: true, 31 | ImportStateVerify: true, 32 | }, 33 | }, 34 | }) 35 | } 36 | 37 | // TODO mavogel: add full network config test in #219 38 | 39 | func testAccNetwork(n string, network *types.NetworkResource) resource.TestCheckFunc { 40 | return func(s *terraform.State) error { 41 | rs, ok := s.RootModule().Resources[n] 42 | if !ok { 43 | return fmt.Errorf("Not found: %s", n) 44 | } 45 | 46 | if rs.Primary.ID == "" { 47 | return fmt.Errorf("No ID is set") 48 | } 49 | 50 | client := testAccProvider.Meta().(*ProviderConfig).DockerClient 51 | networks, err := client.NetworkList(context.Background(), types.NetworkListOptions{}) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | for _, n := range networks { 57 | if n.ID == rs.Primary.ID { 58 | inspected, err := client.NetworkInspect(context.Background(), n.ID, types.NetworkInspectOptions{}) 59 | if err != nil { 60 | return fmt.Errorf("Network could not be obtained: %s", err) 61 | } 62 | *network = inspected 63 | return nil 64 | } 65 | } 66 | 67 | return fmt.Errorf("Network not found: %s", rs.Primary.ID) 68 | } 69 | } 70 | 71 | const testAccDockerNetworkConfig = ` 72 | resource "docker_network" "foo" { 73 | name = "bar" 74 | } 75 | ` 76 | 77 | func TestAccDockerNetwork_internal(t *testing.T) { 78 | var n types.NetworkResource 79 | resourceName := "docker_network.foo" 80 | 81 | resource.Test(t, resource.TestCase{ 82 | PreCheck: func() { testAccPreCheck(t) }, 83 | Providers: testAccProviders, 84 | Steps: []resource.TestStep{ 85 | { 86 | Config: testAccDockerNetworkInternalConfig, 87 | Check: resource.ComposeTestCheckFunc( 88 | testAccNetwork(resourceName, &n), 89 | testAccNetworkInternal(&n, true), 90 | ), 91 | }, 92 | { 93 | ResourceName: resourceName, 94 | ImportState: true, 95 | ImportStateVerify: true, 96 | }, 97 | }, 98 | }) 99 | } 100 | 101 | func testAccNetworkInternal(network *types.NetworkResource, internal bool) resource.TestCheckFunc { 102 | return func(s *terraform.State) error { 103 | if network.Internal != internal { 104 | return fmt.Errorf("Bad value for attribute 'internal': %t", network.Internal) 105 | } 106 | return nil 107 | } 108 | } 109 | 110 | const testAccDockerNetworkInternalConfig = ` 111 | resource "docker_network" "foo" { 112 | name = "bar" 113 | internal = true 114 | } 115 | ` 116 | 117 | func TestAccDockerNetwork_attachable(t *testing.T) { 118 | var n types.NetworkResource 119 | resourceName := "docker_network.foo" 120 | 121 | resource.Test(t, resource.TestCase{ 122 | PreCheck: func() { testAccPreCheck(t) }, 123 | Providers: testAccProviders, 124 | Steps: []resource.TestStep{ 125 | { 126 | Config: testAccDockerNetworkAttachableConfig, 127 | Check: resource.ComposeTestCheckFunc( 128 | testAccNetwork(resourceName, &n), 129 | testAccNetworkAttachable(&n, true), 130 | ), 131 | }, 132 | { 133 | ResourceName: resourceName, 134 | ImportState: true, 135 | ImportStateVerify: true, 136 | }, 137 | }, 138 | }) 139 | } 140 | 141 | func testAccNetworkAttachable(network *types.NetworkResource, attachable bool) resource.TestCheckFunc { 142 | return func(s *terraform.State) error { 143 | if network.Attachable != attachable { 144 | return fmt.Errorf("Bad value for attribute 'attachable': %t", network.Attachable) 145 | } 146 | return nil 147 | } 148 | } 149 | 150 | const testAccDockerNetworkAttachableConfig = ` 151 | resource "docker_network" "foo" { 152 | name = "bar" 153 | attachable = true 154 | } 155 | ` 156 | 157 | //func TestAccDockerNetwork_ingress(t *testing.T) { 158 | // var n types.NetworkResource 159 | // 160 | // resource.Test(t, resource.TestCase{ 161 | // PreCheck: func() { testAccPreCheck(t) }, 162 | // Providers: testAccProviders, 163 | // Steps: []resource.TestStep{ 164 | // resource.TestStep{ 165 | // Config: testAccDockerNetworkIngressConfig, 166 | // Check: resource.ComposeTestCheckFunc( 167 | // testAccNetwork("docker_network.foo", &n), 168 | // testAccNetworkIngress(&n, true), 169 | // ), 170 | // }, 171 | // }, 172 | // }) 173 | //} 174 | // 175 | //func testAccNetworkIngress(network *types.NetworkResource, internal bool) resource.TestCheckFunc { 176 | // return func(s *terraform.State) error { 177 | // if network.Internal != internal { 178 | // return fmt.Errorf("Bad value for attribute 'ingress': %t", network.Ingress) 179 | // } 180 | // return nil 181 | // } 182 | //} 183 | // 184 | //const testAccDockerNetworkIngressConfig = ` 185 | //resource "docker_network" "foo" { 186 | // name = "bar" 187 | // ingress = true 188 | //} 189 | //` 190 | 191 | func TestAccDockerNetwork_ipv4(t *testing.T) { 192 | var n types.NetworkResource 193 | resourceName := "docker_network.foo" 194 | 195 | resource.Test(t, resource.TestCase{ 196 | PreCheck: func() { testAccPreCheck(t) }, 197 | Providers: testAccProviders, 198 | Steps: []resource.TestStep{ 199 | { 200 | Config: testAccDockerNetworkIPv4Config, 201 | Check: resource.ComposeTestCheckFunc( 202 | testAccNetwork(resourceName, &n), 203 | testAccNetworkIPv4(&n, true), 204 | ), 205 | }, 206 | { 207 | ResourceName: resourceName, 208 | ImportState: true, 209 | ImportStateVerify: true, 210 | }, 211 | }, 212 | }) 213 | } 214 | 215 | func testAccNetworkIPv4(network *types.NetworkResource, internal bool) resource.TestCheckFunc { 216 | return func(s *terraform.State) error { 217 | if len(network.IPAM.Config) != 1 { 218 | return fmt.Errorf("Bad value for IPAM configuration count: %d", len(network.IPAM.Config)) 219 | } 220 | if network.IPAM.Config[0].Subnet != "10.0.1.0/24" { 221 | return fmt.Errorf("Bad value for attribute 'subnet': %v", network.IPAM.Config[0].Subnet) 222 | } 223 | return nil 224 | } 225 | } 226 | 227 | const testAccDockerNetworkIPv4Config = ` 228 | resource "docker_network" "foo" { 229 | name = "bar" 230 | ipam_config { 231 | subnet = "10.0.1.0/24" 232 | } 233 | } 234 | ` 235 | 236 | func TestAccDockerNetwork_ipv6(t *testing.T) { 237 | t.Skip("mavogel: need to fix ipv6 network state") 238 | var n types.NetworkResource 239 | resourceName := "docker_network.foo" 240 | 241 | resource.Test(t, resource.TestCase{ 242 | PreCheck: func() { testAccPreCheck(t) }, 243 | Providers: testAccProviders, 244 | Steps: []resource.TestStep{ 245 | { 246 | Config: testAccDockerNetworkIPv6Config, 247 | Check: resource.ComposeTestCheckFunc( 248 | testAccNetwork(resourceName, &n), 249 | testAccNetworkIPv6(&n, true), 250 | ), 251 | }, 252 | // TODO mavogel: ipam config goes from 2->1 253 | // probably suppress diff -> #219 254 | { 255 | ResourceName: resourceName, 256 | ImportState: true, 257 | ImportStateVerify: true, 258 | }, 259 | }, 260 | }) 261 | } 262 | 263 | func testAccNetworkIPv6(network *types.NetworkResource, internal bool) resource.TestCheckFunc { 264 | return func(s *terraform.State) error { 265 | if !network.EnableIPv6 { 266 | return fmt.Errorf("Bad value for attribute 'ipv6': %t", network.EnableIPv6) 267 | } 268 | if len(network.IPAM.Config) != 2 { 269 | return fmt.Errorf("Bad value for IPAM configuration count: %d", len(network.IPAM.Config)) 270 | } 271 | if network.IPAM.Config[1].Subnet != "fd00::1/64" { 272 | return fmt.Errorf("Bad value for attribute 'subnet': %v", network.IPAM.Config[1].Subnet) 273 | } 274 | return nil 275 | } 276 | } 277 | 278 | const testAccDockerNetworkIPv6Config = ` 279 | resource "docker_network" "foo" { 280 | name = "bar" 281 | ipv6 = true 282 | ipam_config { 283 | subnet = "fd00::1/64" 284 | } 285 | # TODO mavogel: Would work but BC - 219 286 | # ipam_config { 287 | # subnet = "10.0.1.0/24" 288 | # } 289 | } 290 | ` 291 | 292 | func TestAccDockerNetwork_labels(t *testing.T) { 293 | var n types.NetworkResource 294 | resourceName := "docker_network.foo" 295 | 296 | resource.Test(t, resource.TestCase{ 297 | PreCheck: func() { testAccPreCheck(t) }, 298 | Providers: testAccProviders, 299 | Steps: []resource.TestStep{ 300 | { 301 | Config: testAccDockerNetworkLabelsConfig, 302 | Check: resource.ComposeTestCheckFunc( 303 | testAccNetwork(resourceName, &n), 304 | testCheckLabelMap(resourceName, "labels", 305 | map[string]string{ 306 | "com.docker.compose.network": "foo", 307 | "com.docker.compose.project": "test", 308 | }, 309 | ), 310 | ), 311 | }, 312 | { 313 | ResourceName: resourceName, 314 | ImportState: true, 315 | ImportStateVerify: true, 316 | }, 317 | }, 318 | }) 319 | } 320 | 321 | func testAccNetworkLabel(network *types.NetworkResource, name string, value string) resource.TestCheckFunc { 322 | return func(s *terraform.State) error { 323 | if network.Labels[name] != value { 324 | return fmt.Errorf("Bad value for label '%s': %s", name, network.Labels[name]) 325 | } 326 | return nil 327 | } 328 | } 329 | 330 | const testAccDockerNetworkLabelsConfig = ` 331 | resource "docker_network" "foo" { 332 | name = "test_foo" 333 | labels { 334 | label = "com.docker.compose.network" 335 | value = "foo" 336 | } 337 | labels { 338 | label = "com.docker.compose.project" 339 | value = "test" 340 | } 341 | } 342 | ` 343 | -------------------------------------------------------------------------------- /docker/resource_docker_registry_image.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 7 | ) 8 | 9 | func resourceDockerRegistryImage() *schema.Resource { 10 | return &schema.Resource{ 11 | Create: resourceDockerRegistryImageCreate, 12 | Read: resourceDockerRegistryImageRead, 13 | Delete: resourceDockerRegistryImageDelete, 14 | Update: resourceDockerRegistryImageUpdate, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "name": { 18 | Type: schema.TypeString, 19 | Required: true, 20 | ForceNew: true, 21 | }, 22 | 23 | "keep_remotely": { 24 | Type: schema.TypeBool, 25 | Optional: true, 26 | Default: false, 27 | }, 28 | 29 | "build": &schema.Schema{ 30 | Type: schema.TypeList, 31 | Optional: true, 32 | MaxItems: 1, 33 | Elem: &schema.Resource{ 34 | Schema: map[string]*schema.Schema{ 35 | "suppress_output": &schema.Schema{ 36 | Type: schema.TypeBool, 37 | Optional: true, 38 | ForceNew: true, 39 | }, 40 | "remote_context": &schema.Schema{ 41 | Type: schema.TypeString, 42 | Optional: true, 43 | ForceNew: true, 44 | }, 45 | "no_cache": &schema.Schema{ 46 | Type: schema.TypeBool, 47 | Optional: true, 48 | ForceNew: true, 49 | }, 50 | "remove": &schema.Schema{ 51 | Type: schema.TypeBool, 52 | Optional: true, 53 | ForceNew: true, 54 | }, 55 | "force_remove": &schema.Schema{ 56 | Type: schema.TypeBool, 57 | Optional: true, 58 | ForceNew: true, 59 | }, 60 | "pull_parent": &schema.Schema{ 61 | Type: schema.TypeBool, 62 | Optional: true, 63 | ForceNew: true, 64 | }, 65 | "isolation": &schema.Schema{ 66 | Type: schema.TypeString, 67 | Optional: true, 68 | ForceNew: true, 69 | }, 70 | "cpu_set_cpus": &schema.Schema{ 71 | Type: schema.TypeString, 72 | Optional: true, 73 | ForceNew: true, 74 | }, 75 | "cpu_set_mems": &schema.Schema{ 76 | Type: schema.TypeString, 77 | Optional: true, 78 | ForceNew: true, 79 | }, 80 | "cpu_shares": &schema.Schema{ 81 | Type: schema.TypeInt, 82 | Optional: true, 83 | ForceNew: true, 84 | }, 85 | "cpu_quota": &schema.Schema{ 86 | Type: schema.TypeInt, 87 | Optional: true, 88 | ForceNew: true, 89 | }, 90 | "cpu_period": &schema.Schema{ 91 | Type: schema.TypeInt, 92 | Optional: true, 93 | ForceNew: true, 94 | }, 95 | "memory": &schema.Schema{ 96 | Type: schema.TypeInt, 97 | Optional: true, 98 | ForceNew: true, 99 | }, 100 | "memory_swap": &schema.Schema{ 101 | Type: schema.TypeInt, 102 | Optional: true, 103 | ForceNew: true, 104 | }, 105 | "cgroup_parent": &schema.Schema{ 106 | Type: schema.TypeString, 107 | Optional: true, 108 | ForceNew: true, 109 | }, 110 | "network_mode": &schema.Schema{ 111 | Type: schema.TypeString, 112 | Optional: true, 113 | ForceNew: true, 114 | }, 115 | "shm_size": &schema.Schema{ 116 | Type: schema.TypeInt, 117 | Optional: true, 118 | ForceNew: true, 119 | }, 120 | "dockerfile": &schema.Schema{ 121 | Type: schema.TypeString, 122 | Optional: true, 123 | Default: "Dockerfile", 124 | ForceNew: true, 125 | }, 126 | "ulimit": &schema.Schema{ 127 | Type: schema.TypeList, 128 | Optional: true, 129 | Elem: &schema.Resource{ 130 | Schema: map[string]*schema.Schema{ 131 | "name": &schema.Schema{ 132 | Type: schema.TypeString, 133 | Required: true, 134 | ForceNew: true, 135 | }, 136 | "hard": &schema.Schema{ 137 | Type: schema.TypeInt, 138 | Required: true, 139 | ForceNew: true, 140 | }, 141 | "soft": &schema.Schema{ 142 | Type: schema.TypeInt, 143 | Required: true, 144 | ForceNew: true, 145 | }, 146 | }, 147 | }, 148 | }, 149 | "build_args": &schema.Schema{ 150 | Type: schema.TypeMap, 151 | Optional: true, 152 | ForceNew: true, 153 | Elem: &schema.Schema{ 154 | Type: schema.TypeString, 155 | }, 156 | }, 157 | "auth_config": &schema.Schema{ 158 | Type: schema.TypeList, 159 | Optional: true, 160 | Elem: &schema.Resource{ 161 | Schema: map[string]*schema.Schema{ 162 | "host_name": &schema.Schema{ 163 | Type: schema.TypeString, 164 | Required: true, 165 | }, 166 | "user_name": &schema.Schema{ 167 | Type: schema.TypeString, 168 | Optional: true, 169 | }, 170 | "password": &schema.Schema{ 171 | Type: schema.TypeString, 172 | Optional: true, 173 | }, 174 | "auth": &schema.Schema{ 175 | Type: schema.TypeString, 176 | Optional: true, 177 | }, 178 | "email": &schema.Schema{ 179 | Type: schema.TypeString, 180 | Optional: true, 181 | }, 182 | "server_address": &schema.Schema{ 183 | Type: schema.TypeString, 184 | Optional: true, 185 | }, 186 | "identity_token": &schema.Schema{ 187 | Type: schema.TypeString, 188 | Optional: true, 189 | }, 190 | "registry_token": &schema.Schema{ 191 | Type: schema.TypeString, 192 | Optional: true, 193 | }, 194 | }, 195 | }, 196 | }, 197 | "context": &schema.Schema{ 198 | Type: schema.TypeString, 199 | Required: true, 200 | ForceNew: true, 201 | StateFunc: func(val interface{}) string { 202 | // the context hash is stored to identify changes in the context files 203 | dockerContextTarPath, _ := buildDockerImageContextTar(val.(string)) 204 | defer os.Remove(dockerContextTarPath) 205 | contextTarHash, _ := getDockerImageContextTarHash(dockerContextTarPath) 206 | return val.(string) + ":" + contextTarHash 207 | }, 208 | }, 209 | "labels": &schema.Schema{ 210 | Type: schema.TypeMap, 211 | Optional: true, 212 | ForceNew: true, 213 | Elem: &schema.Schema{ 214 | Type: schema.TypeString, 215 | }, 216 | }, 217 | "squash": &schema.Schema{ 218 | Type: schema.TypeBool, 219 | Optional: true, 220 | ForceNew: true, 221 | }, 222 | "cache_from": &schema.Schema{ 223 | Type: schema.TypeList, 224 | Optional: true, 225 | ForceNew: true, 226 | Elem: &schema.Schema{ 227 | Type: schema.TypeString, 228 | }, 229 | }, 230 | "security_opt": &schema.Schema{ 231 | Type: schema.TypeList, 232 | Optional: true, 233 | ForceNew: true, 234 | Elem: &schema.Schema{ 235 | Type: schema.TypeString, 236 | }, 237 | }, 238 | "extra_hosts": &schema.Schema{ 239 | Type: schema.TypeList, 240 | Optional: true, 241 | ForceNew: true, 242 | Elem: &schema.Schema{ 243 | Type: schema.TypeString, 244 | }, 245 | }, 246 | "target": &schema.Schema{ 247 | Type: schema.TypeString, 248 | Optional: true, 249 | ForceNew: true, 250 | }, 251 | "session_id": &schema.Schema{ 252 | Type: schema.TypeString, 253 | Optional: true, 254 | ForceNew: true, 255 | }, 256 | "platform": &schema.Schema{ 257 | Type: schema.TypeString, 258 | Optional: true, 259 | ForceNew: true, 260 | }, 261 | "version": &schema.Schema{ 262 | Type: schema.TypeString, 263 | Optional: true, 264 | ForceNew: true, 265 | }, 266 | "build_id": &schema.Schema{ 267 | Type: schema.TypeString, 268 | Optional: true, 269 | ForceNew: true, 270 | }, 271 | }, 272 | }, 273 | }, 274 | 275 | "sha256_digest": { 276 | Type: schema.TypeString, 277 | Computed: true, 278 | }, 279 | }, 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /docker/resource_docker_registry_image_funcs_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/docker/docker/api/types" 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/go-units" 12 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 13 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 14 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 15 | ) 16 | 17 | func TestAccDockerRegistryImageResource_mapping(t *testing.T) { 18 | 19 | assert := func(condition bool, msg string) { 20 | if !condition { 21 | t.Errorf("assertion failed: wrong build parameter %s", msg) 22 | } 23 | } 24 | 25 | dummyProvider := Provider().(*schema.Provider) 26 | dummyResource := dummyProvider.ResourcesMap["docker_registry_image"] 27 | dummyResource.Create = func(d *schema.ResourceData, meta interface{}) error { 28 | build := d.Get("build").([]interface{})[0].(map[string]interface{}) 29 | options := createImageBuildOptions(build) 30 | 31 | assert(options.SuppressOutput == true, "SuppressOutput") 32 | assert(options.RemoteContext == "fooRemoteContext", "RemoteContext") 33 | assert(options.NoCache == true, "NoCache") 34 | assert(options.Remove == true, "Remove") 35 | assert(options.ForceRemove == true, "ForceRemove") 36 | assert(options.PullParent == true, "PullParent") 37 | assert(options.Isolation == container.Isolation("hyperv"), "Isolation") 38 | assert(options.CPUSetCPUs == "fooCpuSetCpus", "CPUSetCPUs") 39 | assert(options.CPUSetMems == "fooCpuSetMems", "CPUSetMems") 40 | assert(options.CPUShares == int64(4), "CPUShares") 41 | assert(options.CPUQuota == int64(5), "CPUQuota") 42 | assert(options.CPUPeriod == int64(6), "CPUPeriod") 43 | assert(options.Memory == int64(1), "Memory") 44 | assert(options.MemorySwap == int64(2), "MemorySwap") 45 | assert(options.CgroupParent == "fooCgroupParent", "CgroupParent") 46 | assert(options.NetworkMode == "fooNetworkMode", "NetworkMode") 47 | assert(options.ShmSize == int64(3), "ShmSize") 48 | assert(options.Dockerfile == "fooDockerfile", "Dockerfile") 49 | assert(len(options.Ulimits) == 1, "Ulimits") 50 | assert(reflect.DeepEqual(*options.Ulimits[0], units.Ulimit{ 51 | Name: "foo", 52 | Hard: int64(1), 53 | Soft: int64(2), 54 | }), "Ulimits") 55 | assert(len(options.BuildArgs) == 1, "BuildArgs") 56 | assert(*options.BuildArgs["HTTP_PROXY"] == "http://10.20.30.2:1234", "BuildArgs") 57 | assert(len(options.AuthConfigs) == 1, "AuthConfigs") 58 | assert(reflect.DeepEqual(options.AuthConfigs["foo.host"], types.AuthConfig{ 59 | Username: "fooUserName", 60 | Password: "fooPassword", 61 | Auth: "fooAuth", 62 | Email: "fooEmail", 63 | ServerAddress: "fooServerAddress", 64 | IdentityToken: "fooIdentityToken", 65 | RegistryToken: "fooRegistryToken", 66 | }), "AuthConfigs") 67 | assert(reflect.DeepEqual(options.Labels, map[string]string{"foo": "bar"}), "Labels") 68 | assert(options.Squash == true, "Squash") 69 | assert(reflect.DeepEqual(options.CacheFrom, []string{"fooCacheFrom", "barCacheFrom"}), "CacheFrom") 70 | assert(reflect.DeepEqual(options.SecurityOpt, []string{"fooSecurityOpt", "barSecurityOpt"}), "SecurityOpt") 71 | assert(reflect.DeepEqual(options.ExtraHosts, []string{"fooExtraHost", "barExtraHost"}), "ExtraHosts") 72 | assert(options.Target == "fooTarget", "Target") 73 | assert(options.SessionID == "fooSessionId", "SessionID") 74 | assert(options.Platform == "fooPlatform", "Platform") 75 | assert(options.Version == types.BuilderVersion("1"), "Version") 76 | assert(options.BuildID == "fooBuildId", "BuildID") 77 | // output 78 | d.SetId("foo") 79 | d.Set("sha256_digest", "bar") 80 | return nil 81 | } 82 | dummyResource.Update = func(d *schema.ResourceData, meta interface{}) error { 83 | return nil 84 | } 85 | dummyResource.Delete = func(d *schema.ResourceData, meta interface{}) error { 86 | return nil 87 | } 88 | dummyResource.Read = func(d *schema.ResourceData, meta interface{}) error { 89 | d.Set("sha256_digest", "bar") 90 | return nil 91 | } 92 | 93 | resource.Test(t, resource.TestCase{ 94 | PreCheck: func() { testAccPreCheck(t) }, 95 | Providers: map[string]terraform.ResourceProvider{"docker": dummyProvider}, 96 | Steps: []resource.TestStep{ 97 | { 98 | Config: testBuildDockerRegistryImageMappingConfig, 99 | Check: resource.ComposeTestCheckFunc( 100 | resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"), 101 | ), 102 | }, 103 | }, 104 | }) 105 | 106 | } 107 | 108 | func TestAccDockerRegistryImageResource_build(t *testing.T) { 109 | pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage:1.0") 110 | context := "../scripts/testing/docker_registry_image_context" 111 | resource.Test(t, resource.TestCase{ 112 | PreCheck: func() { testAccPreCheck(t) }, 113 | Providers: testAccProviders, 114 | CheckDestroy: testDockerRegistryImageNotInRegistry(pushOptions), 115 | Steps: []resource.TestStep{ 116 | { 117 | Config: fmt.Sprintf(testBuildDockerRegistryImageNoKeepConfig, pushOptions.Registry, pushOptions.Name, context), 118 | Check: resource.ComposeTestCheckFunc( 119 | resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"), 120 | ), 121 | }, 122 | }, 123 | }) 124 | } 125 | 126 | func TestAccDockerRegistryImageResource_buildAndKeep(t *testing.T) { 127 | pushOptions := createPushImageOptions("127.0.0.1:15000/tftest-dockerregistryimage:1.0") 128 | context := "../scripts/testing/docker_registry_image_context" 129 | resource.Test(t, resource.TestCase{ 130 | PreCheck: func() { testAccPreCheck(t) }, 131 | Providers: testAccProviders, 132 | CheckDestroy: testDockerRegistryImageInRegistry(pushOptions, true), 133 | Steps: []resource.TestStep{ 134 | { 135 | Config: fmt.Sprintf(testBuildDockerRegistryImageKeepConfig, pushOptions.Registry, pushOptions.Name, context), 136 | Check: resource.ComposeTestCheckFunc( 137 | resource.TestCheckResourceAttrSet("docker_registry_image.foo", "sha256_digest"), 138 | ), 139 | }, 140 | }, 141 | }) 142 | } 143 | 144 | func TestAccDockerRegistryImageResource_pushMissingImage(t *testing.T) { 145 | resource.Test(t, resource.TestCase{ 146 | PreCheck: func() { testAccPreCheck(t) }, 147 | Providers: testAccProviders, 148 | Steps: []resource.TestStep{ 149 | { 150 | Config: testDockerRegistryImagePushMissingConfig, 151 | ExpectError: regexp.MustCompile("An image does not exist locally"), 152 | }, 153 | }, 154 | }) 155 | } 156 | 157 | func testDockerRegistryImageNotInRegistry(pushOpts internalPushImageOptions) resource.TestCheckFunc { 158 | return func(s *terraform.State) error { 159 | providerConfig := testAccProvider.Meta().(*ProviderConfig) 160 | username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig) 161 | digest, _ := getImageDigestWithFallback(pushOpts, username, password) 162 | if digest != "" { 163 | return fmt.Errorf("image found") 164 | } 165 | return nil 166 | } 167 | } 168 | 169 | func testDockerRegistryImageInRegistry(pushOpts internalPushImageOptions, cleanup bool) resource.TestCheckFunc { 170 | return func(s *terraform.State) error { 171 | providerConfig := testAccProvider.Meta().(*ProviderConfig) 172 | username, password := getDockerRegistryImageRegistryUserNameAndPassword(pushOpts, providerConfig) 173 | digest, err := getImageDigestWithFallback(pushOpts, username, password) 174 | if err != nil || len(digest) < 1 { 175 | return fmt.Errorf("image not found") 176 | } 177 | if cleanup { 178 | err := deleteDockerRegistryImage(pushOpts, digest, username, password, false) 179 | if err != nil { 180 | return fmt.Errorf("Unable to remove test image. %s", err) 181 | } 182 | } 183 | return nil 184 | } 185 | } 186 | 187 | const testBuildDockerRegistryImageMappingConfig = ` 188 | resource "docker_registry_image" "foo" { 189 | name = "localhost:15000/foo:1.0" 190 | build { 191 | suppress_output = true 192 | remote_context = "fooRemoteContext" 193 | no_cache = true 194 | remove = true 195 | force_remove = true 196 | pull_parent = true 197 | isolation = "hyperv" 198 | cpu_set_cpus = "fooCpuSetCpus" 199 | cpu_set_mems = "fooCpuSetMems" 200 | cpu_shares = 4 201 | cpu_quota = 5 202 | cpu_period = 6 203 | memory = 1 204 | memory_swap = 2 205 | cgroup_parent = "fooCgroupParent" 206 | network_mode = "fooNetworkMode" 207 | shm_size = 3 208 | dockerfile = "fooDockerfile" 209 | ulimit { 210 | name = "foo" 211 | hard = 1 212 | soft = 2 213 | } 214 | auth_config { 215 | host_name = "foo.host" 216 | user_name = "fooUserName" 217 | password = "fooPassword" 218 | auth = "fooAuth" 219 | email = "fooEmail" 220 | server_address = "fooServerAddress" 221 | identity_token = "fooIdentityToken" 222 | registry_token = "fooRegistryToken" 223 | 224 | } 225 | build_args = { 226 | "HTTP_PROXY" = "http://10.20.30.2:1234" 227 | } 228 | context = "context" 229 | labels = { 230 | foo = "bar" 231 | } 232 | squash = true 233 | cache_from = ["fooCacheFrom", "barCacheFrom"] 234 | security_opt = ["fooSecurityOpt", "barSecurityOpt"] 235 | extra_hosts = ["fooExtraHost", "barExtraHost"] 236 | target = "fooTarget" 237 | session_id = "fooSessionId" 238 | platform = "fooPlatform" 239 | version = "1" 240 | build_id = "fooBuildId" 241 | } 242 | } 243 | ` 244 | 245 | const testBuildDockerRegistryImageNoKeepConfig = ` 246 | provider "docker" { 247 | alias = "private" 248 | registry_auth { 249 | address = "%s" 250 | } 251 | } 252 | resource "docker_registry_image" "foo" { 253 | provider = "docker.private" 254 | name = "%s" 255 | build { 256 | context = "%s" 257 | remove = true 258 | force_remove = true 259 | no_cache = true 260 | } 261 | } 262 | ` 263 | 264 | const testBuildDockerRegistryImageKeepConfig = ` 265 | provider "docker" { 266 | alias = "private" 267 | registry_auth { 268 | address = "%s" 269 | } 270 | } 271 | resource "docker_registry_image" "foo" { 272 | provider = "docker.private" 273 | name = "%s" 274 | keep_remotely = true 275 | build { 276 | context = "%s" 277 | remove = true 278 | force_remove = true 279 | no_cache = true 280 | } 281 | } 282 | ` 283 | 284 | const testDockerRegistryImagePushMissingConfig = ` 285 | provider "docker" { 286 | alias = "private" 287 | registry_auth { 288 | address = "127.0.0.1:15000" 289 | } 290 | } 291 | resource "docker_registry_image" "foo" { 292 | provider = "docker.private" 293 | name = "127.0.0.1:15000/nonexistent:1.0" 294 | } 295 | ` 296 | -------------------------------------------------------------------------------- /docker/resource_docker_secret.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/base64" 5 | "log" 6 | 7 | "context" 8 | 9 | "github.com/docker/docker/api/types/swarm" 10 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 11 | ) 12 | 13 | func resourceDockerSecret() *schema.Resource { 14 | return &schema.Resource{ 15 | Create: resourceDockerSecretCreate, 16 | Read: resourceDockerSecretRead, 17 | Delete: resourceDockerSecretDelete, 18 | 19 | Schema: map[string]*schema.Schema{ 20 | "name": { 21 | Type: schema.TypeString, 22 | Description: "User-defined name of the secret", 23 | Required: true, 24 | ForceNew: true, 25 | }, 26 | 27 | "data": { 28 | Type: schema.TypeString, 29 | Description: "Base64-url-safe-encoded secret data", 30 | Required: true, 31 | Sensitive: true, 32 | ForceNew: true, 33 | ValidateFunc: validateStringIsBase64Encoded(), 34 | }, 35 | 36 | "labels": { 37 | Type: schema.TypeSet, 38 | Optional: true, 39 | ForceNew: true, 40 | Elem: labelSchema, 41 | }, 42 | }, 43 | SchemaVersion: 1, 44 | StateUpgraders: []schema.StateUpgrader{ 45 | { 46 | Version: 0, 47 | Type: resourceDockerSecretV0().CoreConfigSchema().ImpliedType(), 48 | Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { 49 | return replaceLabelsMapFieldWithSetField(rawState), nil 50 | }, 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | func resourceDockerSecretV0() *schema.Resource { 57 | return &schema.Resource{ 58 | //This is only used for state migration, so the CRUD 59 | //callbacks are no longer relevant 60 | Schema: map[string]*schema.Schema{ 61 | "name": { 62 | Type: schema.TypeString, 63 | Description: "User-defined name of the secret", 64 | Required: true, 65 | ForceNew: true, 66 | }, 67 | 68 | "data": { 69 | Type: schema.TypeString, 70 | Description: "User-defined name of the secret", 71 | Required: true, 72 | Sensitive: true, 73 | ForceNew: true, 74 | ValidateFunc: validateStringIsBase64Encoded(), 75 | }, 76 | 77 | "labels": { 78 | Type: schema.TypeMap, 79 | Optional: true, 80 | ForceNew: true, 81 | }, 82 | }, 83 | } 84 | } 85 | 86 | func resourceDockerSecretCreate(d *schema.ResourceData, meta interface{}) error { 87 | client := meta.(*ProviderConfig).DockerClient 88 | data, _ := base64.StdEncoding.DecodeString(d.Get("data").(string)) 89 | 90 | secretSpec := swarm.SecretSpec{ 91 | Annotations: swarm.Annotations{ 92 | Name: d.Get("name").(string), 93 | }, 94 | Data: data, 95 | } 96 | 97 | if v, ok := d.GetOk("labels"); ok { 98 | secretSpec.Annotations.Labels = labelSetToMap(v.(*schema.Set)) 99 | } 100 | 101 | secret, err := client.SecretCreate(context.Background(), secretSpec) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | d.SetId(secret.ID) 107 | 108 | return resourceDockerSecretRead(d, meta) 109 | } 110 | 111 | func resourceDockerSecretRead(d *schema.ResourceData, meta interface{}) error { 112 | client := meta.(*ProviderConfig).DockerClient 113 | secret, _, err := client.SecretInspectWithRaw(context.Background(), d.Id()) 114 | 115 | if err != nil { 116 | log.Printf("[WARN] Secret (%s) not found, removing from state", d.Id()) 117 | d.SetId("") 118 | return nil 119 | } 120 | d.SetId(secret.ID) 121 | d.Set("name", secret.Spec.Name) 122 | // Note mavogel: secret data is not exposed via the API 123 | // TODO next major if we do not explicitly do not store it in the state we could import it, but BC 124 | // d.Set("data", base64.StdEncoding.EncodeToString(secret.Spec.Data)) 125 | return nil 126 | } 127 | 128 | func resourceDockerSecretDelete(d *schema.ResourceData, meta interface{}) error { 129 | client := meta.(*ProviderConfig).DockerClient 130 | err := client.SecretRemove(context.Background(), d.Id()) 131 | 132 | if err != nil { 133 | return err 134 | } 135 | 136 | d.SetId("") 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /docker/resource_docker_secret_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 11 | ) 12 | 13 | func TestAccDockerSecret_basic(t *testing.T) { 14 | resource.Test(t, resource.TestCase{ 15 | PreCheck: func() { testAccPreCheck(t) }, 16 | Providers: testAccProviders, 17 | CheckDestroy: testCheckDockerSecretDestroy, 18 | Steps: []resource.TestStep{ 19 | { 20 | Config: ` 21 | resource "docker_secret" "foo" { 22 | name = "foo-secret" 23 | data = "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA==" 24 | } 25 | `, 26 | Check: resource.ComposeTestCheckFunc( 27 | resource.TestCheckResourceAttr("docker_secret.foo", "name", "foo-secret"), 28 | resource.TestCheckResourceAttr("docker_secret.foo", "data", "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA=="), 29 | ), 30 | }, 31 | }, 32 | }) 33 | } 34 | 35 | func TestAccDockerSecret_basicUpdatable(t *testing.T) { 36 | resource.Test(t, resource.TestCase{ 37 | PreCheck: func() { testAccPreCheck(t) }, 38 | Providers: testAccProviders, 39 | CheckDestroy: testCheckDockerSecretDestroy, 40 | Steps: []resource.TestStep{ 41 | { 42 | Config: ` 43 | resource "docker_secret" "foo" { 44 | name = "tftest-mysecret-${replace(timestamp(),":", ".")}" 45 | data = "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA==" 46 | 47 | lifecycle { 48 | ignore_changes = ["name"] 49 | create_before_destroy = true 50 | } 51 | } 52 | `, 53 | Check: resource.ComposeTestCheckFunc( 54 | resource.TestCheckResourceAttr("docker_secret.foo", "data", "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA=="), 55 | ), 56 | }, 57 | { 58 | Config: ` 59 | resource "docker_secret" "foo" { 60 | name = "tftest-mysecret2-${replace(timestamp(),":", ".")}" 61 | data = "U3VuIDI1IE1hciAyMDE4IDE0OjUzOjIxIENFU1QK" 62 | 63 | lifecycle { 64 | ignore_changes = ["name"] 65 | create_before_destroy = true 66 | } 67 | } 68 | `, 69 | Check: resource.ComposeTestCheckFunc( 70 | resource.TestCheckResourceAttr("docker_secret.foo", "data", "U3VuIDI1IE1hciAyMDE4IDE0OjUzOjIxIENFU1QK"), 71 | ), 72 | }, 73 | }, 74 | }) 75 | } 76 | 77 | func TestAccDockerSecret_labels(t *testing.T) { 78 | resource.Test(t, resource.TestCase{ 79 | PreCheck: func() { testAccPreCheck(t) }, 80 | Providers: testAccProviders, 81 | CheckDestroy: testCheckDockerSecretDestroy, 82 | Steps: []resource.TestStep{ 83 | { 84 | Config: ` 85 | resource "docker_secret" "foo" { 86 | name = "foo-secret" 87 | data = "Ymxhc2RzYmxhYmxhMTI0ZHNkd2VzZA==" 88 | labels { 89 | label = "test1" 90 | value = "foo" 91 | } 92 | labels { 93 | label = "test2" 94 | value = "bar" 95 | } 96 | } 97 | `, 98 | Check: testCheckLabelMap("docker_secret.foo", "labels", 99 | map[string]string{ 100 | "test1": "foo", 101 | "test2": "bar", 102 | }, 103 | ), 104 | }, 105 | }, 106 | }) 107 | } 108 | 109 | ///////////// 110 | // Helpers 111 | ///////////// 112 | func testCheckDockerSecretDestroy(s *terraform.State) error { 113 | client := testAccProvider.Meta().(*ProviderConfig).DockerClient 114 | for _, rs := range s.RootModule().Resources { 115 | if rs.Type != "secrets" { 116 | continue 117 | } 118 | 119 | id := rs.Primary.Attributes["id"] 120 | _, _, err := client.SecretInspectWithRaw(context.Background(), id) 121 | 122 | if err == nil { 123 | return fmt.Errorf("Secret with id '%s' still exists", id) 124 | } 125 | return nil 126 | } 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /docker/resource_docker_volume.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "github.com/docker/docker/api/types" 11 | "github.com/docker/docker/api/types/volume" 12 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 13 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 14 | ) 15 | 16 | func resourceDockerVolume() *schema.Resource { 17 | return &schema.Resource{ 18 | Create: resourceDockerVolumeCreate, 19 | Read: resourceDockerVolumeRead, 20 | Delete: resourceDockerVolumeDelete, 21 | Importer: &schema.ResourceImporter{ 22 | State: schema.ImportStatePassthrough, 23 | }, 24 | 25 | Schema: map[string]*schema.Schema{ 26 | "name": { 27 | Type: schema.TypeString, 28 | Optional: true, 29 | Computed: true, 30 | ForceNew: true, 31 | }, 32 | "labels": { 33 | Type: schema.TypeSet, 34 | Optional: true, 35 | ForceNew: true, 36 | Elem: labelSchema, 37 | }, 38 | "driver": { 39 | Type: schema.TypeString, 40 | Optional: true, 41 | Computed: true, 42 | ForceNew: true, 43 | }, 44 | "driver_opts": { 45 | Type: schema.TypeMap, 46 | Optional: true, 47 | ForceNew: true, 48 | }, 49 | "mountpoint": { 50 | Type: schema.TypeString, 51 | Computed: true, 52 | }, 53 | }, 54 | SchemaVersion: 1, 55 | StateUpgraders: []schema.StateUpgrader{ 56 | { 57 | Version: 0, 58 | Type: resourceDockerVolumeV0().CoreConfigSchema().ImpliedType(), 59 | Upgrade: func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { 60 | return replaceLabelsMapFieldWithSetField(rawState), nil 61 | }, 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | func resourceDockerVolumeV0() *schema.Resource { 68 | return &schema.Resource{ 69 | //This is only used for state migration, so the CRUD 70 | //callbacks are no longer relevant 71 | Schema: map[string]*schema.Schema{ 72 | "name": { 73 | Type: schema.TypeString, 74 | Optional: true, 75 | Computed: true, 76 | ForceNew: true, 77 | }, 78 | "labels": { 79 | Type: schema.TypeMap, 80 | Optional: true, 81 | ForceNew: true, 82 | }, 83 | "driver": { 84 | Type: schema.TypeString, 85 | Optional: true, 86 | Computed: true, 87 | ForceNew: true, 88 | }, 89 | "driver_opts": { 90 | Type: schema.TypeMap, 91 | Optional: true, 92 | ForceNew: true, 93 | }, 94 | "mountpoint": { 95 | Type: schema.TypeString, 96 | Computed: true, 97 | }, 98 | }, 99 | } 100 | } 101 | 102 | func resourceDockerVolumeCreate(d *schema.ResourceData, meta interface{}) error { 103 | client := meta.(*ProviderConfig).DockerClient 104 | ctx := context.Background() 105 | 106 | createOpts := volume.VolumeCreateBody{} 107 | 108 | if v, ok := d.GetOk("name"); ok { 109 | createOpts.Name = v.(string) 110 | } 111 | if v, ok := d.GetOk("labels"); ok { 112 | createOpts.Labels = labelSetToMap(v.(*schema.Set)) 113 | } 114 | if v, ok := d.GetOk("driver"); ok { 115 | createOpts.Driver = v.(string) 116 | } 117 | if v, ok := d.GetOk("driver_opts"); ok { 118 | createOpts.DriverOpts = mapTypeMapValsToString(v.(map[string]interface{})) 119 | } 120 | 121 | var err error 122 | var retVolume types.Volume 123 | retVolume, err = client.VolumeCreate(ctx, createOpts) 124 | 125 | if err != nil { 126 | return fmt.Errorf("Unable to create volume: %s", err) 127 | } 128 | 129 | d.SetId(retVolume.Name) 130 | return resourceDockerVolumeRead(d, meta) 131 | } 132 | 133 | func resourceDockerVolumeRead(d *schema.ResourceData, meta interface{}) error { 134 | client := meta.(*ProviderConfig).DockerClient 135 | ctx := context.Background() 136 | 137 | var err error 138 | var retVolume types.Volume 139 | retVolume, err = client.VolumeInspect(ctx, d.Id()) 140 | 141 | if err != nil { 142 | return fmt.Errorf("Unable to inspect volume: %s", err) 143 | } 144 | 145 | d.Set("name", retVolume.Name) 146 | d.Set("labels", mapToLabelSet(retVolume.Labels)) 147 | d.Set("driver", retVolume.Driver) 148 | d.Set("driver_opts", retVolume.Options) 149 | d.Set("mountpoint", retVolume.Mountpoint) 150 | 151 | return nil 152 | } 153 | 154 | func resourceDockerVolumeDelete(d *schema.ResourceData, meta interface{}) error { 155 | log.Printf("[INFO] Waiting for volume: '%s' to get removed: max '%v seconds'", d.Id(), 30) 156 | 157 | stateConf := &resource.StateChangeConf{ 158 | Pending: []string{"in_use"}, 159 | Target: []string{"removed"}, 160 | Refresh: resourceDockerVolumeRemoveRefreshFunc(d.Id(), meta), 161 | Timeout: 30 * time.Second, 162 | MinTimeout: 5 * time.Second, 163 | Delay: 2 * time.Second, 164 | } 165 | 166 | // Wait, catching any errors 167 | _, err := stateConf.WaitForState() 168 | if err != nil { 169 | return err 170 | } 171 | 172 | d.SetId("") 173 | return nil 174 | } 175 | 176 | func resourceDockerVolumeRemoveRefreshFunc( 177 | volumeID string, meta interface{}) resource.StateRefreshFunc { 178 | return func() (interface{}, string, error) { 179 | client := meta.(*ProviderConfig).DockerClient 180 | forceDelete := true 181 | 182 | if err := client.VolumeRemove(context.Background(), volumeID, forceDelete); err != nil { 183 | if strings.Contains(err.Error(), "volume is in use") { // store.IsInUse(err) 184 | log.Printf("[INFO] Volume with id '%v' is still in use", volumeID) 185 | return volumeID, "in_use", nil 186 | } 187 | log.Printf("[INFO] Removing volume with id '%v' caused an error: %v", volumeID, err) 188 | return nil, "", err 189 | } 190 | log.Printf("[INFO] Removing volume with id '%v' got removed", volumeID) 191 | return volumeID, "removed", nil 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /docker/resource_docker_volume_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/docker/docker/api/types" 9 | "github.com/hashicorp/terraform-plugin-sdk/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/terraform" 11 | ) 12 | 13 | func TestAccDockerVolume_basic(t *testing.T) { 14 | var v types.Volume 15 | 16 | resource.Test(t, resource.TestCase{ 17 | PreCheck: func() { testAccPreCheck(t) }, 18 | Providers: testAccProviders, 19 | Steps: []resource.TestStep{ 20 | { 21 | Config: testAccDockerVolumeConfig, 22 | Check: resource.ComposeTestCheckFunc( 23 | checkDockerVolume("docker_volume.foo", &v), 24 | resource.TestCheckResourceAttr("docker_volume.foo", "id", "testAccDockerVolume_basic"), 25 | resource.TestCheckResourceAttr("docker_volume.foo", "name", "testAccDockerVolume_basic"), 26 | ), 27 | }, 28 | { 29 | ResourceName: "docker_volume.foo", 30 | ImportState: true, 31 | ImportStateVerify: true, 32 | }, 33 | }, 34 | }) 35 | } 36 | 37 | func checkDockerVolume(n string, volume *types.Volume) resource.TestCheckFunc { 38 | return func(s *terraform.State) error { 39 | rs, ok := s.RootModule().Resources[n] 40 | if !ok { 41 | return fmt.Errorf("Not found: %s", n) 42 | } 43 | 44 | if rs.Primary.ID == "" { 45 | return fmt.Errorf("No ID is set") 46 | } 47 | 48 | ctx := context.Background() 49 | client := testAccProvider.Meta().(*ProviderConfig).DockerClient 50 | v, err := client.VolumeInspect(ctx, rs.Primary.ID) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | *volume = v 56 | 57 | return nil 58 | } 59 | } 60 | 61 | const testAccDockerVolumeConfig = ` 62 | resource "docker_volume" "foo" { 63 | name = "testAccDockerVolume_basic" 64 | } 65 | ` 66 | 67 | func TestAccDockerVolume_labels(t *testing.T) { 68 | var v types.Volume 69 | 70 | resource.Test(t, resource.TestCase{ 71 | PreCheck: func() { testAccPreCheck(t) }, 72 | Providers: testAccProviders, 73 | Steps: []resource.TestStep{ 74 | { 75 | Config: testAccDockerVolumeLabelsConfig, 76 | Check: resource.ComposeTestCheckFunc( 77 | checkDockerVolume("docker_volume.foo", &v), 78 | testCheckLabelMap("docker_volume.foo", "labels", 79 | map[string]string{ 80 | "com.docker.compose.project": "test", 81 | "com.docker.compose.volume": "foo", 82 | }, 83 | ), 84 | ), 85 | }, 86 | { 87 | ResourceName: "docker_volume.foo", 88 | ImportState: true, 89 | ImportStateVerify: true, 90 | }, 91 | }, 92 | }) 93 | } 94 | 95 | func testAccVolumeLabel(volume *types.Volume, name string, value string) resource.TestCheckFunc { 96 | return func(s *terraform.State) error { 97 | if volume.Labels[name] != value { 98 | return fmt.Errorf("Bad value for label '%s': %s", name, volume.Labels[name]) 99 | } 100 | return nil 101 | } 102 | } 103 | 104 | const testAccDockerVolumeLabelsConfig = ` 105 | resource "docker_volume" "foo" { 106 | name = "test_foo" 107 | labels { 108 | label = "com.docker.compose.project" 109 | value = "test" 110 | } 111 | labels { 112 | label = "com.docker.compose.volume" 113 | value = "foo" 114 | } 115 | } 116 | ` 117 | -------------------------------------------------------------------------------- /docker/validators.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/helper/schema" 11 | ) 12 | 13 | func validateIntegerInRange(min, max int) schema.SchemaValidateFunc { 14 | return func(v interface{}, k string) (ws []string, errors []error) { 15 | value := v.(int) 16 | if value < min { 17 | errors = append(errors, fmt.Errorf( 18 | "%q cannot be lower than %d: %d", k, min, value)) 19 | } 20 | if value > max { 21 | errors = append(errors, fmt.Errorf( 22 | "%q cannot be higher than %d: %d", k, max, value)) 23 | } 24 | return 25 | } 26 | } 27 | 28 | func validateIntegerGeqThan(threshold int) schema.SchemaValidateFunc { 29 | return func(v interface{}, k string) (ws []string, errors []error) { 30 | value := v.(int) 31 | if value < threshold { 32 | errors = append(errors, fmt.Errorf( 33 | "%q cannot be lower than %d", k, threshold)) 34 | } 35 | return 36 | } 37 | } 38 | 39 | func validateFloatRatio() schema.SchemaValidateFunc { 40 | return func(v interface{}, k string) (ws []string, errors []error) { 41 | value := v.(float64) 42 | if value < 0.0 || value > 1.0 { 43 | errors = append(errors, fmt.Errorf( 44 | "%q has to be between 0.0 and 1.0", k)) 45 | } 46 | return 47 | } 48 | } 49 | 50 | func validateStringIsFloatRatio() schema.SchemaValidateFunc { 51 | return func(v interface{}, k string) (ws []string, errors []error) { 52 | switch v.(type) { 53 | case string: 54 | stringValue := v.(string) 55 | value, err := strconv.ParseFloat(stringValue, 64) 56 | if err != nil { 57 | errors = append(errors, fmt.Errorf( 58 | "%q is not a float", k)) 59 | } 60 | if value < 0.0 || value > 1.0 { 61 | errors = append(errors, fmt.Errorf( 62 | "%q has to be between 0.0 and 1.0", k)) 63 | } 64 | case int: 65 | value := float64(v.(int)) 66 | if value < 0.0 || value > 1.0 { 67 | errors = append(errors, fmt.Errorf( 68 | "%q has to be between 0.0 and 1.0", k)) 69 | } 70 | default: 71 | errors = append(errors, fmt.Errorf( 72 | "%q is not a string", k)) 73 | } 74 | return 75 | } 76 | } 77 | 78 | func validateDurationGeq0() schema.SchemaValidateFunc { 79 | return func(v interface{}, k string) (ws []string, errors []error) { 80 | value := v.(string) 81 | dur, err := time.ParseDuration(value) 82 | if err != nil { 83 | errors = append(errors, fmt.Errorf( 84 | "%q is not a valid duration", k)) 85 | } 86 | if dur < 0 { 87 | errors = append(errors, fmt.Errorf( 88 | "duration must not be negative")) 89 | } 90 | return 91 | } 92 | } 93 | 94 | func validateStringMatchesPattern(pattern string) schema.SchemaValidateFunc { 95 | return func(v interface{}, k string) (ws []string, errors []error) { 96 | compiledRegex, err := regexp.Compile(pattern) 97 | if err != nil { 98 | errors = append(errors, fmt.Errorf( 99 | "%q regex does not compile", pattern)) 100 | return 101 | } 102 | 103 | value := v.(string) 104 | if !compiledRegex.MatchString(value) { 105 | errors = append(errors, fmt.Errorf( 106 | "%q doesn't match the pattern (%q): %q", 107 | k, pattern, value)) 108 | } 109 | 110 | return 111 | } 112 | } 113 | 114 | func validateStringIsBase64Encoded() schema.SchemaValidateFunc { 115 | return func(v interface{}, k string) (ws []string, errors []error) { 116 | value := v.(string) 117 | if _, err := base64.StdEncoding.DecodeString(value); err != nil { 118 | errors = append(errors, fmt.Errorf( 119 | "%q is not base64 decodeable", k)) 120 | } 121 | 122 | return 123 | } 124 | } 125 | 126 | func validateDockerContainerPath(v interface{}, k string) (ws []string, errors []error) { 127 | 128 | value := v.(string) 129 | if !regexp.MustCompile(`^[a-zA-Z]:\\|^/`).MatchString(value) { 130 | errors = append(errors, fmt.Errorf("%q must be an absolute path", k)) 131 | } 132 | 133 | return 134 | } 135 | -------------------------------------------------------------------------------- /docker/validators_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "testing" 4 | 5 | func TestValidateIntegerInRange(t *testing.T) { 6 | validIntegers := []int{-259, 0, 1, 5, 999} 7 | min := -259 8 | max := 999 9 | for _, v := range validIntegers { 10 | _, errors := validateIntegerInRange(min, max)(v, "name") 11 | if len(errors) != 0 { 12 | t.Fatalf("%q should be an integer in range (%d, %d): %q", v, min, max, errors) 13 | } 14 | } 15 | 16 | invalidIntegers := []int{-260, -99999, 1000, 25678} 17 | for _, v := range invalidIntegers { 18 | _, errors := validateIntegerInRange(min, max)(v, "name") 19 | if len(errors) == 0 { 20 | t.Fatalf("%q should be an integer outside range (%d, %d)", v, min, max) 21 | } 22 | } 23 | } 24 | 25 | func TestValidateIntegerGeqThan0(t *testing.T) { 26 | v := 1 27 | if _, error := validateIntegerGeqThan(0)(v, "name"); error != nil { 28 | t.Fatalf("%q should be an integer greater than 0", v) 29 | } 30 | 31 | v = -4 32 | if _, error := validateIntegerGeqThan(0)(v, "name"); error == nil { 33 | t.Fatalf("%q should be an invalid integer smaller than 0", v) 34 | } 35 | } 36 | 37 | func TestValidateFloatRatio(t *testing.T) { 38 | v := 0.9 39 | if _, error := validateFloatRatio()(v, "name"); error != nil { 40 | t.Fatalf("%v should be a float between 0.0 and 1.0", v) 41 | } 42 | 43 | v = -4.5 44 | if _, error := validateFloatRatio()(v, "name"); error == nil { 45 | t.Fatalf("%v should be an invalid float smaller than 0.0", v) 46 | } 47 | 48 | v = 1.1 49 | if _, error := validateFloatRatio()(v, "name"); error == nil { 50 | t.Fatalf("%v should be an invalid float greater than 1.0", v) 51 | } 52 | } 53 | func TestValidateStringIsFloatRatio(t *testing.T) { 54 | v := "0.9" 55 | if _, error := validateStringIsFloatRatio()(v, "name"); error != nil { 56 | t.Fatalf("%v should be a float between 0.0 and 1.0", v) 57 | } 58 | 59 | v = "-4.5" 60 | if _, error := validateStringIsFloatRatio()(v, "name"); error == nil { 61 | t.Fatalf("%v should be an invalid float smaller than 0.0", v) 62 | } 63 | 64 | v = "1.1" 65 | if _, error := validateStringIsFloatRatio()(v, "name"); error == nil { 66 | t.Fatalf("%v should be an invalid float greater than 1.0", v) 67 | } 68 | v = "false" 69 | if _, error := validateStringIsFloatRatio()(v, "name"); error == nil { 70 | t.Fatalf("%v should be an invalid float because it is a bool in a string", v) 71 | } 72 | w := false 73 | if _, error := validateStringIsFloatRatio()(w, "name"); error == nil { 74 | t.Fatalf("%v should be an invalid float because it is a bool", v) 75 | } 76 | i := 0 77 | if _, error := validateStringIsFloatRatio()(i, "name"); error != nil { 78 | t.Fatalf("%v should be a valid float because int can be casted", v) 79 | } 80 | i = 1 81 | if _, error := validateStringIsFloatRatio()(i, "name"); error != nil { 82 | t.Fatalf("%v should be a valid float because int can be casted", v) 83 | } 84 | i = 4 85 | if _, error := validateStringIsFloatRatio()(i, "name"); error == nil { 86 | t.Fatalf("%v should be an invalid float because it is an int out of range", v) 87 | } 88 | } 89 | func TestValidateDurationGeq0(t *testing.T) { 90 | v := "1ms" 91 | if _, error := validateDurationGeq0()(v, "name"); error != nil { 92 | t.Fatalf("%v should be a valid durarion", v) 93 | } 94 | 95 | v = "-2h" 96 | if _, error := validateDurationGeq0()(v, "name"); error == nil { 97 | t.Fatalf("%v should be an invalid duration smaller than 0", v) 98 | } 99 | } 100 | 101 | func TestValidateStringMatchesPattern(t *testing.T) { 102 | pattern := `^(pause|continue-mate|break)$` 103 | v := "pause" 104 | if _, error := validateStringMatchesPattern(pattern)(v, "name"); error != nil { 105 | t.Fatalf("%q should match the pattern", v) 106 | } 107 | v = "doesnotmatch" 108 | if _, error := validateStringMatchesPattern(pattern)(v, "name"); error == nil { 109 | t.Fatalf("%q should not match the pattern", v) 110 | } 111 | v = "continue-mate" 112 | if _, error := validateStringMatchesPattern(pattern)(v, "name"); error != nil { 113 | t.Fatalf("%q should match the pattern", v) 114 | } 115 | } 116 | 117 | func TestValidateStringShouldBeBase64Encoded(t *testing.T) { 118 | v := `YmtzbGRrc2xka3NkMjM4MQ==` 119 | if _, error := validateStringIsBase64Encoded()(v, "name"); error != nil { 120 | t.Fatalf("%q should be base64 decodeable", v) 121 | } 122 | 123 | v = `%&df#3NkMjM4MQ==` 124 | if _, error := validateStringIsBase64Encoded()(v, "name"); error == nil { 125 | t.Fatalf("%q should NOT be base64 decodeable", v) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /examples/ssh-protocol/README.md: -------------------------------------------------------------------------------- 1 | ## Using and testing the ssh-protocol 2 | 3 | The `ssh://` which was introduced in docker can be tested/used as shown in this example. 4 | 5 | ```sh 6 | # export your pub key(s) in terraform pub_key variable 7 | export TF_VAR_pub_key="$(cat ~/.ssh/*.pub)" 8 | 9 | # launch dind container with ssh and docker accepting your PK for root user 10 | terraform apply -target docker_container.dind 11 | 12 | # wait for few seconds/minutes 13 | 14 | # ssh to container to remember server keys 15 | ssh root@localhost -p 32822 uptime 16 | 17 | # test docker host ssh protocol 18 | terraform apply -target docker_image.test 19 | ``` -------------------------------------------------------------------------------- /examples/ssh-protocol/main.tf: -------------------------------------------------------------------------------- 1 | # test case 2 | provider "docker" { 3 | version = "~> 1.2.0" 4 | alias = "test" 5 | 6 | host = "ssh://root@localhost:32822" 7 | } 8 | 9 | resource "docker_image" "test" { 10 | provider = "docker.test" 11 | name = "busybox:latest" 12 | } 13 | 14 | # scaffolding 15 | variable "pub_key" { 16 | type = "string" 17 | } 18 | 19 | provider "docker" { 20 | version = "~> 1.2.0" 21 | } 22 | 23 | resource "docker_image" "dind" { 24 | name = "docker:18.09.0-dind" 25 | } 26 | 27 | resource "docker_container" "dind" { 28 | depends_on = [ 29 | "docker_image.dind", 30 | ] 31 | 32 | name = "dind" 33 | image = "docker:18.09.0-dind" 34 | 35 | privileged = true 36 | 37 | start = true 38 | 39 | command = ["/bin/sh", "-c", 40 | < /etc/conf.d/docker 51 | echo DOCKERD_OPTS=--host=unix:///var/run/docker.sock >> /etc/conf.d/docker 52 | rc-update add docker 53 | 54 | # setup ssh for root 55 | mkdir -p ~/.ssh 56 | 57 | # link docker cli so root can see it 58 | ln -s /usr/local/bin/docker /usr/bin/ 59 | 60 | # start ssh and docker 61 | exec /sbin/init 62 | SH 63 | , 64 | ] 65 | 66 | ports { 67 | internal = 22 68 | external = 32822 69 | } 70 | 71 | upload { 72 | content = < /dev/null; then 6 | GO111MODULE=off go get -u github.com/mitchellh/gox 7 | fi 8 | 9 | # setup environment 10 | PROVIDER_NAME="docker" 11 | TARGET_DIR="$(pwd)/results" 12 | XC_ARCH=${XC_ARCH:-"386 amd64 arm"} 13 | XC_OS=${XC_OS:=linux darwin windows openbsd solaris} 14 | XC_EXCLUDE_OSARCH="!darwin/arm !darwin/386 !solaris/amd64" 15 | LD_FLAGS="-s -w" 16 | export CGO_ENABLED=0 17 | 18 | rm -rf "${TARGET_DIR}" 19 | mkdir -p "${TARGET_DIR}" 20 | 21 | # Compile 22 | gox \ 23 | -os="${XC_OS}" \ 24 | -arch="${XC_ARCH}" \ 25 | -osarch="${XC_EXCLUDE_OSARCH}" \ 26 | -ldflags "${LD_FLAGS}" \ 27 | -output "$TARGET_DIR/{{.OS}}_{{.Arch}}/terraform-provider-${PROVIDER_NAME}_v0.0.0_x4" \ 28 | -verbose \ 29 | -rebuild \ 30 | . 31 | -------------------------------------------------------------------------------- /scripts/errcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking for unchecked errors..." 5 | 6 | if ! which errcheck > /dev/null; then 7 | echo "==> Installing errcheck..." 8 | go get -u github.com/kisielk/errcheck 9 | fi 10 | 11 | err_files=$(errcheck -ignoretests \ 12 | -ignore 'github.com/hashicorp/terraform/helper/schema:Set' \ 13 | -ignore 'bytes:.*' \ 14 | -ignore 'io:Close|Write' \ 15 | $(go list ./...| grep -v /vendor/)) 16 | 17 | if [[ -n ${err_files} ]]; then 18 | echo 'Unchecked errors found in the following places:' 19 | echo "${err_files}" 20 | echo "Please handle returned errors. You can check directly with \`make errcheck\`" 21 | exit 1 22 | fi 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /scripts/gofmtcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking that code complies with gofmt requirements..." 5 | gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) 6 | if [[ -n ${gofmt_files} ]]; then 7 | echo 'gofmt needs running on the following files:' 8 | echo "${gofmt_files}" 9 | echo "You can use the command: \`make fmt\` to reformat code." 10 | exit 1 11 | fi 12 | 13 | exit 0 14 | -------------------------------------------------------------------------------- /scripts/gogetcookie.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch ~/.gitcookies 4 | chmod 0600 ~/.gitcookies 5 | 6 | git config --global http.cookiefile ~/.gitcookies 7 | 8 | tr , \\t <<\__END__ >>~/.gitcookies 9 | .googlesource.com,TRUE,/,TRUE,2147483647,o,git-paul.hashicorp.com=1/z7s05EYPudQ9qoe6dMVfmAVwgZopEkZBb1a2mA5QtHE 10 | __END__ 11 | -------------------------------------------------------------------------------- /scripts/runAccTests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | :: As of `go-dockerclient` v1.2.0, the default endpoint to the Docker daemon 5 | :: is a UNIX socket. We need to force it to use the Windows named pipe when 6 | :: running against Docker for Windows. 7 | set DOCKER_HOST=npipe:////.//pipe//docker_engine 8 | 9 | :: Note: quoting these values breaks the tests! 10 | set DOCKER_REGISTRY_ADDRESS=127.0.0.1:15000 11 | set DOCKER_REGISTRY_USER=testuser 12 | set DOCKER_REGISTRY_PASS=testpwd 13 | set DOCKER_PRIVATE_IMAGE=127.0.0.1:15000/tftest-service:v1 14 | set TF_ACC=1 15 | 16 | call:setup 17 | if %ErrorLevel% neq 0 ( 18 | call:print "Failed to set up acceptance test fixtures." 19 | exit /b %ErrorLevel% 20 | ) 21 | 22 | call:run 23 | if %ErrorLevel% neq 0 ( 24 | call:print "Acceptance tests failed." 25 | set outcome=1 26 | ) else ( 27 | set outcome=0 28 | ) 29 | 30 | call:cleanup 31 | if %ErrorLevel% neq 0 ( 32 | call:print "Failed to clean up acceptance test fixtures." 33 | exit /b %ErrorLevel% 34 | ) 35 | 36 | exit /b %outcome% 37 | 38 | 39 | :print 40 | if "%~1" == "" ( 41 | echo. 42 | ) else ( 43 | echo %~1 44 | ) 45 | exit /b 0 46 | 47 | 48 | :log 49 | call:print "" 50 | call:print "##################################" 51 | call:print "-------- %~1" 52 | call:print "##################################" 53 | exit /b 0 54 | 55 | 56 | :setup 57 | call:log "setup" 58 | call %~dp0testing\setup_private_registry.bat 59 | exit /b %ErrorLevel% 60 | 61 | 62 | :run 63 | call:log "run" 64 | call go test ./docker -v -timeout 120m 65 | exit /b %ErrorLevel% 66 | 67 | 68 | :cleanup 69 | call:log "cleanup" 70 | call:print "### unsetted env ###" 71 | for /F %%p in ('docker container ls -f "name=private_registry" -q') do ( 72 | call docker stop %%p 73 | call docker rm -f -v %%p 74 | ) 75 | call:print "### stopped private registry ###" 76 | rmdir /q /s %~dp0testing\auth 77 | rmdir /q /s %~dp0testing\certs 78 | call:print "### removed auth and certs ###" 79 | for %%r in ("container" "volume") do ( 80 | call docker %%r ls -f "name=tftest-" -q 81 | for /F %%i in ('docker %%r ls -f "name=tf-test" -q') do ( 82 | echo Deleting %%r %%i 83 | call docker %%r rm -f -v %%i 84 | ) 85 | for /F %%i in ('docker %%r ls -f "name=tftest-" -q') do ( 86 | echo Deleting %%r %%i 87 | call docker %%r rm -f -v %%i 88 | ) 89 | call:print "### removed %%r ###" 90 | ) 91 | for %%r in ("config" "secret" "network") do ( 92 | call docker %%r ls -f "name=tftest-" -q 93 | for /F %%i in ('docker %%r ls -f "name=tftest-" -q') do ( 94 | echo Deleting %%r %%i 95 | call docker %%r rm %%i 96 | ) 97 | call:print "### removed %%r ###" 98 | ) 99 | for /F %%i in ('docker images -aq 127.0.0.1:5000/tftest-service') do ( 100 | echo Deleting imag %%i 101 | docker rmi -f %%i 102 | ) 103 | call:print "### removed service images ###" 104 | exit /b %ErrorLevel% 105 | -------------------------------------------------------------------------------- /scripts/testacc_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | for p in $(docker container ls -f 'name=private_registry' -q); do docker stop $p; done 5 | echo "### stopped private registry ###" 6 | 7 | rm -f "$(pwd)/scripts/testing/testingFile" 8 | rm -f "$(pwd)/scripts/testing/testingFile.base64" 9 | rm -f "$(pwd)"/scripts/testing/auth/htpasswd 10 | rm -f "$(pwd)"/scripts/testing/certs/registry_auth.* 11 | echo "### removed auth and certs ###" 12 | 13 | for resource in "container" "volume"; do 14 | for r in $(docker $resource ls -f 'name=tftest-' -q); do docker $resource rm -f "$r"; done 15 | echo "### removed $resource ###" 16 | done 17 | 18 | for resource in "config" "secret" "network"; do 19 | for r in $(docker $resource ls -f 'name=tftest-' -q); do docker $resource rm "$r"; done 20 | echo "### removed $resource ###" 21 | done 22 | 23 | for i in $(docker images -aq 127.0.0.1:15000/tftest-service); do docker rmi -f "$i"; done 24 | echo "### removed service images ###" 25 | -------------------------------------------------------------------------------- /scripts/testacc_full.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | log() { 5 | echo "####################" 6 | echo "## -> $1 " 7 | echo "####################" 8 | } 9 | 10 | setup() { 11 | sh "$(pwd)"/scripts/testacc_setup.sh 12 | } 13 | 14 | run() { 15 | go clean -testcache 16 | TF_ACC=1 go test ./docker -v -timeout 120m 17 | 18 | # keep the return value for the scripts to fail and clean properly 19 | return $? 20 | } 21 | 22 | cleanup() { 23 | sh "$(pwd)"/scripts/testacc_cleanup.sh 24 | } 25 | 26 | ## main 27 | log "setup" && setup 28 | log "run" && run || (log "cleanup" && cleanup && exit 1) 29 | log "cleanup" && cleanup 30 | -------------------------------------------------------------------------------- /scripts/testacc_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo -n "foo" > "$(pwd)/scripts/testing/testingFile" 5 | echo -n `base64 $(pwd)/scripts/testing/testingFile` > "$(pwd)/scripts/testing/testingFile.base64" 6 | 7 | # Create self signed certs 8 | mkdir -p "$(pwd)"/scripts/testing/certs 9 | openssl req \ 10 | -newkey rsa:2048 \ 11 | -nodes \ 12 | -x509 \ 13 | -days 365 \ 14 | -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=127.0.0.1" \ 15 | -keyout "$(pwd)"/scripts/testing/certs/registry_auth.key \ 16 | -out "$(pwd)"/scripts/testing/certs/registry_auth.crt 17 | # Create auth 18 | mkdir -p "$(pwd)"/scripts/testing/auth 19 | # Start registry 20 | # pinned to 2.7.0 due to https://github.com/docker/docker.github.io/issues/11060 21 | docker run --rm --entrypoint htpasswd registry:2.7.0 -Bbn testuser testpwd > "$(pwd)"/scripts/testing/auth/htpasswd 22 | docker run -d -p 15000:5000 --rm --name private_registry \ 23 | -v "$(pwd)"/scripts/testing/auth:/auth \ 24 | -e "REGISTRY_AUTH=htpasswd" \ 25 | -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \ 26 | -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \ 27 | -v "$(pwd)"/scripts/testing/certs:/certs \ 28 | -e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry_auth.crt" \ 29 | -e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" \ 30 | -e "REGISTRY_STORAGE_DELETE_ENABLED=true" \ 31 | registry:2.7.0 32 | # wait a bit for travis... 33 | sleep 5 34 | # Login to private registry 35 | docker login -u testuser -p testpwd 127.0.0.1:15000 36 | # Build private images 37 | for i in $(seq 1 3); do 38 | docker build -t tftest-service --build-arg JS_FILE_PATH=server_v${i}.js "$(pwd)"/scripts/testing -f "$(pwd)"/scripts/testing/Dockerfile 39 | docker tag tftest-service 127.0.0.1:15000/tftest-service:v${i} 40 | docker push 127.0.0.1:15000/tftest-service:v${i} 41 | docker tag tftest-service 127.0.0.1:15000/tftest-service 42 | docker push 127.0.0.1:15000/tftest-service 43 | done 44 | # Remove images from host machine before starting the tests 45 | for i in $(docker images -aq 127.0.0.1:15000/tftest-service); do docker rmi -f "$i"; done 46 | -------------------------------------------------------------------------------- /scripts/testing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.12.3-slim 2 | 3 | ARG JS_FILE_PATH 4 | 5 | COPY configs.json . 6 | COPY secrets.json . 7 | COPY $JS_FILE_PATH server.js 8 | 9 | CMD [ "node", "server.js" ] 10 | 11 | EXPOSE 8080 12 | -------------------------------------------------------------------------------- /scripts/testing/configs.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "123" 3 | } -------------------------------------------------------------------------------- /scripts/testing/docker_registry_image_context/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY empty /empty -------------------------------------------------------------------------------- /scripts/testing/docker_registry_image_context/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/terraform-provider-docker/6b1ca2cbc187f7bc3124c6b30cd73efaacdad400/scripts/testing/docker_registry_image_context/empty -------------------------------------------------------------------------------- /scripts/testing/dockerconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | "127.0.0.1:15000": { 4 | "auth": "dGVzdHVzZXI6dGVzdHB3ZA==" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /scripts/testing/secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "QWERTY" 3 | } -------------------------------------------------------------------------------- /scripts/testing/server_v1.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var configs = require('./configs') 3 | var secrets = require('./secrets') 4 | 5 | var handleRequest = function(request, response) { 6 | console.log('Received request for URL: ' + request.url); 7 | 8 | if(request.url === '/health') { 9 | response.writeHead(200); 10 | response.end('ok'); 11 | } else { 12 | response.writeHead(200); 13 | response.end(configs.prefix + ' - Hello World!'); 14 | } 15 | }; 16 | var www = http.createServer(handleRequest); 17 | www.listen(8080); 18 | -------------------------------------------------------------------------------- /scripts/testing/server_v2.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var configs = require('./configs') 3 | var secrets = require('./secrets') 4 | 5 | var handleRequest = function(request, response) { 6 | console.log('Received request for URL: ' + request.url); 7 | 8 | if(request.url === '/health') { 9 | response.writeHead(200); 10 | response.end('ok'); 11 | } else if(request.url === '/newroute') { 12 | response.writeHead(200); 13 | response.end('new Route!'); 14 | } else { 15 | response.writeHead(200); 16 | response.end(configs.prefix + ' - Hello World!'); 17 | } 18 | }; 19 | var www = http.createServer(handleRequest); 20 | www.listen(8080); 21 | -------------------------------------------------------------------------------- /scripts/testing/server_v3.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var configs = require('./configs') 3 | var secrets = require('./secrets') 4 | 5 | var handleRequest = function (request, response) { 6 | console.log('Received request for URL: ' + request.url); 7 | response.writeHead(200); 8 | response.end(configs.prefix + ' - Hello World!'); 9 | }; 10 | var www = http.createServer(handleRequest); 11 | www.listen(8085); // changed here on purpose 12 | -------------------------------------------------------------------------------- /scripts/testing/setup_private_registry.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | :: Create self-signed certificate. 5 | call:mkdirp %~dp0certs 6 | call openssl req ^ 7 | -newkey rsa:2048 ^ 8 | -nodes ^ 9 | -x509 ^ 10 | -days 365 ^ 11 | -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=127.0.0.1" ^ 12 | -keyout %~dp0certs\registry_auth.key ^ 13 | -out %~dp0certs\registry_auth.crt 14 | if %ErrorLevel% neq 0 ( 15 | call:print "Failed to generate self-signed certificate." 16 | exit /b %ErrorLevel% 17 | ) 18 | 19 | :: Generate random credentials. 20 | call:mkdirp %~dp0auth 21 | call docker run ^ 22 | --rm ^ 23 | --entrypoint htpasswd ^ 24 | registry:2 ^ 25 | -Bbn testuser testpwd ^ 26 | > %~dp0auth\htpasswd 27 | if %ErrorLevel% neq 0 ( 28 | call:print "Failed to generate random credentials." 29 | exit /b %ErrorLevel% 30 | ) 31 | 32 | :: Start an ephemeral Docker registry in a container. 33 | :: --rm ^ 34 | @echo on 35 | call docker run ^ 36 | -d ^ 37 | --name private_registry ^ 38 | -p 15000:5000 ^ 39 | -v %~dp0auth:/auth ^ 40 | -e "REGISTRY_AUTH=htpasswd" ^ 41 | -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" ^ 42 | -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" ^ 43 | -v %~dp0certs:/certs ^ 44 | -e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry_auth.crt" ^ 45 | -e "REGISTRY_HTTP_TLS_KEY=/certs/registry_auth.key" ^ 46 | registry:2 47 | if %ErrorLevel% neq 0 ( 48 | call:print "Failed to create ephemeral Docker registry." 49 | exit /b %ErrorLevel% 50 | ) 51 | 52 | :: Wait until the container is responsive (*crosses fingers*). 53 | timeout /t 5 54 | 55 | :: Point our Docker Daemon to this ephemeral registry. 56 | call docker login -u testuser -p testpwd 127.0.0.1:15000 57 | if %ErrorLevel% neq 0 ( 58 | call:print "Failed to log in to ephemeral Docker registry." 59 | exit /b %ErrorLevel% 60 | ) 61 | 62 | :: Build a few private images. 63 | for /L %%i in (1,1,3) do ( 64 | call docker build ^ 65 | -t tftest-service ^ 66 | --build-arg JS_FILE_PATH=server_v%%i.js ^ 67 | %~dp0 ^ 68 | -f %~dp0Dockerfile 69 | call docker tag ^ 70 | tftest-service ^ 71 | 127.0.0.1:15000/tftest-service:v%%i 72 | call docker push ^ 73 | 127.0.0.1:15000/tftest-service:v%%i 74 | ) 75 | 76 | exit /b %ErrorLevel% 77 | 78 | 79 | :print 80 | echo %~1 81 | exit /b 0 82 | 83 | 84 | :mkdirp 85 | if not exist %~1\nul ( 86 | mkdir %~1 87 | ) 88 | exit /b %ErrorLevel% 89 | -------------------------------------------------------------------------------- /website/docker.erb: -------------------------------------------------------------------------------- 1 | <% wrap_layout :inner do %> 2 | <% content_for :sidebar do %> 3 | 64 | <% end %> 65 | 66 | <%= yield %> 67 | <% end %> 68 | -------------------------------------------------------------------------------- /website/docs/d/docker_network.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_network" 4 | sidebar_current: "docs-docker-datasource-docker-network" 5 | description: |- 6 | `docker_network` provides details about a specific Docker Network. 7 | --- 8 | 9 | # docker\_network 10 | 11 | Finds a specific docker network and returns information about it. 12 | 13 | ## Example Usage 14 | 15 | ```hcl 16 | data "docker_network" "main" { 17 | name = "main" 18 | } 19 | ``` 20 | 21 | ## Argument Reference 22 | 23 | The following arguments are supported: 24 | 25 | * `name` - (Optional, string) The name of the Docker network. 26 | * `id` - (Optional, string) The id of the Docker network. 27 | 28 | ## Attributes Reference 29 | 30 | The following attributes are exported in addition to the above configuration: 31 | 32 | * `driver` - (Optional, string) The driver of the Docker network. 33 | Possible values are `bridge`, `host`, `overlay`, `macvlan`. 34 | See [docker docs][networkdocs] for more details. 35 | * `options` - (Optional, map) Only available with bridge networks. See 36 | [docker docs][bridgeoptionsdocs] for more details. 37 | * `internal` (Optional, bool) Boolean flag for whether the network is internal. 38 | * `ipam_config` (Optional, map) See [IPAM](#ipam) below for details. 39 | * `scope` (Optional, string) Scope of the network. One of `swarm`, `global`, or `local`. 40 | 41 | [networkdocs] https://docs.docker.com/network/#network-drivers 42 | [bridgeoptionsdocs] https://docs.docker.com/engine/reference/commandline/network_create/#bridge-driver-options -------------------------------------------------------------------------------- /website/docs/d/registry_image.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_registry_image" 4 | sidebar_current: "docs-docker-datasource-registry-image" 5 | description: |- 6 | Finds the latest available sha256 digest for a docker image/tag from a registry. 7 | --- 8 | 9 | # docker\_registry\_image 10 | 11 | Reads the image metadata from a Docker Registry. Used in conjunction with the 12 | [docker\_image](/docs/providers/docker/r/image.html) resource to keep an image up 13 | to date on the latest available version of the tag. 14 | 15 | ## Example Usage 16 | 17 | ```hcl 18 | data "docker_registry_image" "ubuntu" { 19 | name = "ubuntu:precise" 20 | } 21 | 22 | resource "docker_image" "ubuntu" { 23 | name = "${data.docker_registry_image.ubuntu.name}" 24 | pull_triggers = ["${data.docker_registry_image.ubuntu.sha256_digest}"] 25 | } 26 | ``` 27 | 28 | ## Argument Reference 29 | 30 | The following arguments are supported: 31 | 32 | * `name` - (Required, string) The name of the Docker image, including any tags. e.g. `alpine:latest` 33 | 34 | ## Attributes Reference 35 | 36 | The following attributes are exported in addition to the above configuration: 37 | 38 | * `sha256_digest` (string) - The content digest of the image, as stored on the registry. 39 | -------------------------------------------------------------------------------- /website/docs/index.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Provider: Docker" 4 | sidebar_current: "docs-docker-index" 5 | description: |- 6 | The Docker provider is used to interact with Docker containers and images. 7 | --- 8 | 9 | # Docker Provider 10 | 11 | The Docker provider is used to interact with Docker containers and images. 12 | It uses the Docker API to manage the lifecycle of Docker containers. Because 13 | the Docker provider uses the Docker API, it is immediately compatible not 14 | only with single server Docker but Swarm and any additional Docker-compatible 15 | API hosts. 16 | 17 | Use the navigation to the left to read about the available resources. 18 | 19 | ## Example Usage 20 | 21 | ```hcl 22 | # Configure the Docker provider 23 | provider "docker" { 24 | host = "tcp://127.0.0.1:2376/" 25 | } 26 | 27 | # Create a container 28 | resource "docker_container" "foo" { 29 | image = "${docker_image.ubuntu.latest}" 30 | name = "foo" 31 | } 32 | 33 | resource "docker_image" "ubuntu" { 34 | name = "ubuntu:latest" 35 | } 36 | ``` 37 | 38 | -> **Note** 39 | You can also use the `ssh` protocol to connect to the docker host on a remote machine. 40 | The configuration would look as follows: 41 | 42 | ```hcl 43 | provider "docker" { 44 | host = "ssh://user@remote-host:22" 45 | } 46 | ``` 47 | 48 | ## Registry Credentials 49 | 50 | Registry credentials can be provided on a per-registry basis with the `registry_auth` 51 | field, passing either a config file or the username/password directly. 52 | 53 | -> **Note** 54 | The location of the config file is on the machine terraform runs on, nevertheless if the specified docker host is on another machine. 55 | 56 | ``` hcl 57 | provider "docker" { 58 | host = "tcp://localhost:2376" 59 | 60 | registry_auth { 61 | address = "registry.hub.docker.com" 62 | config_file = "${pathexpand("~/.docker/config.json")}" 63 | } 64 | 65 | registry_auth { 66 | address = "registry.my.company.com" 67 | config_file_content = "${var.plain_content_of_config_file}" 68 | } 69 | 70 | registry_auth { 71 | address = "quay.io:8181" 72 | username = "someuser" 73 | password = "somepass" 74 | } 75 | } 76 | 77 | data "docker_registry_image" "quay" { 78 | name = "myorg/privateimage" 79 | } 80 | 81 | data "docker_registry_image" "quay" { 82 | name = "quay.io:8181/myorg/privateimage" 83 | } 84 | ``` 85 | 86 | -> **Note** 87 | When passing in a config file either the corresponding `auth` string of the repository is read or the os specific 88 | credential helpers (see [here](https://github.com/docker/docker-credential-helpers#available-programs)) are 89 | used to retrieve the authentication credentials. 90 | 91 | You can still use the enviroment variables `DOCKER_REGISTRY_USER` and `DOCKER_REGISTRY_PASS`. 92 | 93 | An example content of the file `~/.docker/config.json` on macOS may look like follows: 94 | 95 | ```json 96 | { 97 | "auths": { 98 | "repo.mycompany:8181": { 99 | "auth": "dXNlcjpwYXNz=" 100 | }, 101 | "otherrepo.other-company:8181": { 102 | 103 | } 104 | }, 105 | "credsStore" : "osxkeychain" 106 | } 107 | ``` 108 | 109 | ## Certificate information 110 | 111 | Specify certificate information either with a directory or 112 | directly with the content of the files for connecting to the Docker host via TLS. 113 | 114 | ```hcl 115 | provider "docker" { 116 | host = "tcp://your-host-ip:2376/" 117 | 118 | # -> specify either 119 | cert_path = "${pathexpand("~/.docker")}" 120 | 121 | # -> or the following 122 | ca_material = "${file(pathexpand("~/.docker/ca.pem"))}" # this can be omitted 123 | cert_material = "${file(pathexpand("~/.docker/cert.pem"))}" 124 | key_material = "${file(pathexpand("~/.docker/key.pem"))}" 125 | } 126 | ``` 127 | 128 | ## Argument Reference 129 | 130 | The following arguments are supported: 131 | 132 | * `host` - (Required) This is the address to the Docker host. If this is 133 | blank, the `DOCKER_HOST` environment variable will also be read. 134 | 135 | * `cert_path` - (Optional) Path to a directory with certificate information 136 | for connecting to the Docker host via TLS. It is expected that the 3 files `{ca, cert, key}.pem` 137 | are present in the path. If the path is blank, the `DOCKER_CERT_PATH` will also be checked. 138 | 139 | * `ca_material`, `cert_material`, `key_material`, - (Optional) Content of `ca.pem`, `cert.pem`, and `key.pem` files 140 | for TLS authentication. Cannot be used together with `cert_path`. If `ca_material` is omitted 141 | the client does not check the servers certificate chain and host name. 142 | 143 | * `registry_auth` - (Optional) A block specifying the credentials for a target 144 | v2 Docker registry. 145 | 146 | * `address` - (Required) The address of the registry. 147 | 148 | * `username` - (Optional) The username to use for authenticating to the registry. 149 | Cannot be used with the `config_file` option. If this is blank, the `DOCKER_REGISTRY_USER` 150 | will also be checked. 151 | 152 | * `password` - (Optional) The password to use for authenticating to the registry. 153 | Cannot be used with the `config_file` option. If this is blank, the `DOCKER_REGISTRY_PASS` 154 | will also be checked. 155 | 156 | * `config_file` - (Optional) The path to a config file containing credentials for 157 | authenticating to the registry. Cannot be used with the `username`/`password` or `config_file_content` options. 158 | If this is blank, the `DOCKER_CONFIG` will also be checked. 159 | 160 | * `config_file_content` - (Optional) The content of a config file as string containing credentials for 161 | authenticating to the registry. Cannot be used with the `username`/`password` or `config_file` options. 162 | 163 | 164 | 165 | ~> **NOTE on Certificates and `docker-machine`:** As per [Docker Remote API 166 | documentation](https://docs.docker.com/engine/reference/api/docker_remote_api/), 167 | in any docker-machine environment, the Docker daemon uses an encrypted TCP 168 | socket (TLS) and requires `cert_path` for a successful connection. As an alternative, 169 | if using `docker-machine`, run `eval $(docker-machine env )` prior 170 | to running Terraform, and the host and certificate path will be extracted from 171 | the environment. 172 | -------------------------------------------------------------------------------- /website/docs/r/config.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_config" 4 | sidebar_current: "docs-docker-resource-config" 5 | description: |- 6 | Manages the configs of a Docker service in a swarm. 7 | --- 8 | 9 | # docker\_config 10 | 11 | Manages the configuration of a Docker service in a swarm. 12 | 13 | ## Example Usage 14 | 15 | ## Basic 16 | ```hcl 17 | # Creates a config 18 | resource "docker_config" "foo_config" { 19 | name = "foo_config" 20 | data = "ewogICJzZXJIfQo=" 21 | } 22 | ``` 23 | 24 | ### Advanced 25 | #### Dynamically set config with a template 26 | In this example you can use the `${var.foo_port}` variable to dynamically 27 | set the `${port}` variable in the `foo.configs.json.tpl` template and create 28 | the data of the `foo_config` with the help of the `base64encode` interpolation 29 | function. 30 | 31 | File `foo.config.json.tpl` 32 | 33 | ```json 34 | { 35 | "server": { 36 | "public_port": ${port} 37 | } 38 | } 39 | ``` 40 | 41 | File `main.tf` 42 | 43 | ```hcl 44 | # Creates the template in renders the variable 45 | data "template_file" "foo_config_tpl" { 46 | template = "${file("foo.config.json.tpl")}" 47 | 48 | vars { 49 | port = "${var.foo_port}" 50 | } 51 | } 52 | 53 | # Creates the config 54 | resource "docker_config" "foo_config" { 55 | name = "foo_config" 56 | data = "${base64encode(data.template_file.foo_config_tpl.rendered)}" 57 | } 58 | ``` 59 | 60 | #### Update config with no downtime 61 | To update a `config`, Terraform will destroy the existing resource and create a replacement. To effectively use a `docker_config` resource with a `docker_service` resource, it's recommended to specify `create_before_destroy` in a `lifecycle` block. Provide a unique `name` attribute, for example 62 | with one of the interpolation functions `uuid` or `timestamp` as shown 63 | in the example below. The reason is [moby-35803](https://github.com/moby/moby/issues/35803). 64 | 65 | ```hcl 66 | resource "docker_config" "service_config" { 67 | name = "${var.service_name}-config-${replace(timestamp(),":", ".")}" 68 | data = "${base64encode(data.template_file.service_config_tpl.rendered)}" 69 | 70 | lifecycle { 71 | ignore_changes = ["name"] 72 | create_before_destroy = true 73 | } 74 | } 75 | 76 | resource "docker_service" "service" { 77 | # ... 78 | configs = [ 79 | { 80 | config_id = "${docker_config.service_config.id}" 81 | config_name = "${docker_config.service_config.name}" 82 | file_name = "/root/configs/configs.json" 83 | }, 84 | ] 85 | } 86 | ``` 87 | 88 | ## Argument Reference 89 | 90 | The following arguments are supported: 91 | 92 | * `name` - (Required, string) The name of the Docker config. 93 | * `data` - (Required, string) The base64 encoded data of the config. 94 | 95 | 96 | ## Attributes Reference 97 | 98 | The following attributes are exported in addition to the above configuration: 99 | 100 | * `id` (string) 101 | 102 | ## Import 103 | 104 | Docker config can be imported using the long id, e.g. for a config with the short id `p73jelnrme5f`: 105 | 106 | ```sh 107 | $ terraform import docker_config.foo $(docker config inspect -f {{.ID}} p73) 108 | ``` 109 | -------------------------------------------------------------------------------- /website/docs/r/image.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_image" 4 | sidebar_current: "docs-docker-resource-image" 5 | description: |- 6 | Pulls a Docker image to a given Docker host. 7 | --- 8 | 9 | # docker\_image 10 | 11 | Pulls a Docker image to a given Docker host from a Docker Registry. 12 | 13 | This resource will *not* pull new layers of the image automatically unless used in 14 | conjunction with [`docker_registry_image`](/docs/providers/docker/d/registry_image.html) 15 | data source to update the `pull_triggers` field. 16 | 17 | ## Example Usage 18 | 19 | ```hcl 20 | # Find the latest Ubuntu precise image. 21 | resource "docker_image" "ubuntu" { 22 | name = "ubuntu:precise" 23 | } 24 | 25 | # Access it somewhere else with ${docker_image.ubuntu.latest} 26 | 27 | ``` 28 | 29 | ### Dynamic image 30 | 31 | ```hcl 32 | data "docker_registry_image" "ubuntu" { 33 | name = "ubuntu:precise" 34 | } 35 | 36 | resource "docker_image" "ubuntu" { 37 | name = "${data.docker_registry_image.ubuntu.name}" 38 | pull_triggers = ["${data.docker_registry_image.ubuntu.sha256_digest}"] 39 | } 40 | ``` 41 | 42 | ## Argument Reference 43 | 44 | The following arguments are supported: 45 | 46 | * `name` - (Required, string) The name of the Docker image, including any tags or SHA256 repo digests. 47 | * `keep_locally` - (Optional, boolean) If true, then the Docker image won't be 48 | deleted on destroy operation. If this is false, it will delete the image from 49 | the docker local storage on destroy operation. 50 | * `pull_triggers` - (Optional, list of strings) List of values which cause an 51 | image pull when changed. This is used to store the image digest from the 52 | registry when using the `docker_registry_image` [data source](/docs/providers/docker/d/registry_image.html) 53 | to trigger an image update. 54 | * `pull_trigger` - **Deprecated**, use `pull_triggers` instead. 55 | * `build` - (Optional, block) See [Build](#build-1) below for details. 56 | 57 | 58 | ### Build 59 | Build image. 60 | 61 | The `build` block supports: 62 | 63 | * `path` - (Required, string) 64 | * `dockerfile` - (Optional, string) default Dockerfile 65 | * `tag` - (Optional, list of strings) 66 | * `force_remove` - (Optional, boolean) 67 | * `remove` - (Optional, boolean) default true 68 | * `no_cache` - (Optional, boolean) 69 | * `target` - (Optional, string) 70 | * `build_arg` - (Optional, map of strings) 71 | * `label` - (Optional, map of strings) 72 | 73 | ## Attributes Reference 74 | 75 | The following attributes are exported in addition to the above configuration: 76 | 77 | * `latest` (string) - The ID of the image. 78 | -------------------------------------------------------------------------------- /website/docs/r/network.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_network" 4 | sidebar_current: "docs-docker-resource-network" 5 | description: |- 6 | Manages a Docker Network. 7 | --- 8 | 9 | # docker\_network 10 | 11 | Manages a Docker Network. This can be used alongside 12 | [docker\_container](/docs/providers/docker/r/container.html) 13 | to create virtual networks within the docker environment. 14 | 15 | ## Example Usage 16 | 17 | ```hcl 18 | # Create a new docker network 19 | resource "docker_network" "private_network" { 20 | name = "my_network" 21 | } 22 | 23 | # Access it somewhere else with ${docker_network.private_network.name} 24 | 25 | ``` 26 | 27 | ## Argument Reference 28 | 29 | The following arguments are supported: 30 | 31 | * `name` - (Required, string) The name of the Docker network. 32 | * `labels` - (Optional, block) See [Labels](#labels-1) below for details. 33 | * `check_duplicate` - (Optional, boolean) Requests daemon to check for networks 34 | with same name. 35 | * `driver` - (Optional, string) Name of the network driver to use. Defaults to 36 | `bridge` driver. 37 | * `options` - (Optional, map of strings) Network specific options to be used by 38 | the drivers. 39 | * `internal` - (Optional, boolean) Restrict external access to the network. 40 | Defaults to `false`. 41 | * `attachable` - (Optional, boolean) Enable manual container attachment to the network. 42 | Defaults to `false`. 43 | * `ingress` - (Optional, boolean) Create swarm routing-mesh network. 44 | Defaults to `false`. 45 | * `ipv6` - (Optional, boolean) Enable IPv6 networking. 46 | Defaults to `false`. 47 | * `ipam_driver` - (Optional, string) Driver used by the custom IP scheme of the 48 | network. 49 | * `ipam_config` - (Optional, block) See [IPAM config](#ipam_config-1) below for 50 | details. 51 | 52 | 53 | #### Labels 54 | 55 | `labels` is a block within the configuration that can be repeated to specify 56 | additional label name and value data to the container. Each `labels` block supports 57 | the following: 58 | 59 | * `label` - (Required, string) Name of the label 60 | * `value` (Required, string) Value of the label 61 | 62 | See [214](https://github.com/terraform-providers/terraform-provider-docker/issues/214#issuecomment-550128950) for Details. 63 | 64 | 65 | ### IPAM config 66 | Configuration of the custom IP scheme of the network. 67 | 68 | The `ipam_config` block supports: 69 | 70 | * `subnet` - (Optional, string) 71 | * `ip_range` - (Optional, string) 72 | * `gateway` - (Optional, string) 73 | * `aux_address` - (Optional, map of string) 74 | 75 | ## Attributes Reference 76 | 77 | The following attributes are exported in addition to the above configuration: 78 | 79 | * `id` (string) 80 | * `scope` (string) 81 | 82 | ## Import 83 | 84 | Docker networks can be imported using the long id, e.g. for a network with the short id `p73jelnrme5f`: 85 | 86 | ```sh 87 | $ terraform import docker_network.foo $(docker network inspect -f {{.ID}} p73) 88 | ``` -------------------------------------------------------------------------------- /website/docs/r/registry_image.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_registry_image" 4 | sidebar_current: "docs-docker-resource-registry-image" 5 | description: |- 6 | Manages the lifecycle of docker image/tag in a registry. 7 | --- 8 | 9 | # docker\_registry\_image 10 | 11 | Provides an image/tag in a Docker registry. 12 | 13 | ## Example Usage 14 | 15 | ```hcl 16 | resource "docker_registry_image" "helloworld" { 17 | 18 | name = "helloworld:1.0" 19 | 20 | build { 21 | context = "pathToContextFolder" 22 | } 23 | 24 | } 25 | 26 | ``` 27 | 28 | ## Argument Reference 29 | 30 | * `name` - (Required, string) The name of the Docker image. 31 | * `keep_remotely` - (Optional, boolean) If true, then the Docker image won't be 32 | deleted on destroy operation. If this is false, it will delete the image from 33 | the docker registry on destroy operation. 34 | 35 | * `build` - (Optional, Map) See [Build](#build-1) below for details. 36 | 37 | 38 | #### Build Block 39 | 40 | * `context` (Required, string) - The path to the context folder 41 | * `suppress_output` (Optional, bool) - Suppress the build output and print image ID on success 42 | * `remote_context` (Optional, string) - A Git repository URI or HTTP/HTTPS context URI 43 | * `no_cache` (Optional, bool) - Do not use the cache when building the image 44 | * `remove` (Optional, bool) - Remove intermediate containers after a successful build (default behavior) 45 | * `force_remove` (Optional, bool) - Always remove intermediate containers 46 | * `pull_parent` (Optional, bool) - Attempt to pull the image even if an older image exists locally 47 | * `isolation` (Optional, string) - Isolation represents the isolation technology of a container. The supported values are platform specific 48 | * `cpu_set_cpus` (Optional, string) - CPUs in which to allow execution (e.g., 0-3, 0,1) 49 | * `cpu_set_mems` (Optional, string) - MEMs in which to allow execution (0-3, 0,1) 50 | * `cpu_shares` (Optional, int) - CPU shares (relative weight) 51 | * `cpu_quota` (Optional, int) - Microseconds of CPU time that the container can get in a CPU period 52 | * `cpu_period` (Optional, int) - The length of a CPU period in microseconds 53 | * `memory` (Optional, int) - Set memory limit for build 54 | * `memory_swap` (Optional, int) - Total memory (memory + swap), -1 to enable unlimited swap 55 | * `cgroup_parent` (Optional, string) - Optional parent cgroup for the container 56 | * `network_mode` (Optional, string) - Set the networking mode for the RUN instructions during build 57 | * `shm_size` (Optional, int) - Size of /dev/shm in bytes. The size must be greater than 0 58 | * `` (Optional, string) - Set the networking mode for the RUN instructions during build 59 | * `dockerfile` (Optional, string) - Dockerfile file. Default is "Dockerfile" 60 | * `ulimit` (Optional, Map) - See [Ulimit](#ulimit-1) below for details 61 | * `build_args` (Optional, map of key/value pairs) string pairs for build-time variables 62 | * `auth_config` (Optional, Map) - See [AuthConfig](#authconfig-1) below for details 63 | * `labels` (Optional, map of key/value pairs) string pairs for labels 64 | * `squash` (Optional, bool) - squash the new layers into a new image with a single new layer 65 | * `cache_from` (Optional, []string) - Images to consider as cache sources 66 | * `security_opt` (Optional, []string) - Security options 67 | * `extra_hosts` (Optional, []string) - A list of hostnames/IP mappings to add to the container’s /etc/hosts file. Specified in the form ["hostname:IP"] 68 | * `target` (Optional, string) - Set the target build stage to build 69 | * `platform` (Optional, string) - Set platform if server is multi-platform capable 70 | * `version` (Optional, string) - Version of the unerlying builder to use 71 | * `build_id` (Optional, string) - BuildID is an optional identifier that can be passed together with the build request. The same identifier can be used to gracefully cancel the build with the cancel request 72 | 73 | 74 | #### Ulimit Block 75 | 76 | * `name` - (Required, string) type of ulimit, e.g. nofile 77 | * `soft` (Required, int) - soft limit 78 | * `hard` (Required, int) - hard limit 79 | 80 | 81 | #### AuthConfig Block 82 | 83 | * `host_name` - (Required, string) hostname of the registry 84 | * `user_name` - (Optional, string) the registry user name 85 | * `password` - (Optional, string) the registry password 86 | * `auth` - (Optional, string) the auth token 87 | * `email` - (Optional, string) the user emal 88 | * `server_address` - (Optional, string) the server address 89 | * `identity_token` - (Optional, string) the identity token 90 | * `registry_token` - (Optional, string) the registry token 91 | 92 | ## Attributes Reference 93 | 94 | The following attributes are exported in addition to the above configuration: 95 | 96 | * `sha256_digest` (string) - The sha256 digest of the image. 97 | -------------------------------------------------------------------------------- /website/docs/r/secret.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_secret" 4 | sidebar_current: "docs-docker-resource-secret" 5 | description: |- 6 | Manages the secrets of a Docker service in a swarm. 7 | --- 8 | 9 | # docker\_secret 10 | 11 | Manages the secrets of a Docker service in a swarm. 12 | 13 | ## Example Usage 14 | 15 | ### Basic 16 | 17 | ```hcl 18 | # Creates a secret 19 | resource "docker_secret" "foo_secret" { 20 | name = "foo_secret" 21 | data = "ewogICJzZXJsaasIfQo=" 22 | } 23 | ``` 24 | 25 | #### Update secret with no downtime 26 | To update a `secret`, Terraform will destroy the existing resource and create a replacement. To effectively use a `docker_secret` resource with a `docker_service` resource, it's recommended to specify `create_before_destroy` in a `lifecycle` block. Provide a unique `name` attribute, for example 27 | with one of the interpolation functions `uuid` or `timestamp` as shown 28 | in the example below. The reason is [moby-35803](https://github.com/moby/moby/issues/35803). 29 | 30 | ```hcl 31 | resource "docker_secret" "service_secret" { 32 | name = "${var.service_name}-secret-${replace(timestamp(),":", ".")}" 33 | data = "${base64encode(data.template_file.service_secret_tpl.rendered)}" 34 | 35 | lifecycle { 36 | ignore_changes = ["name"] 37 | create_before_destroy = true 38 | } 39 | } 40 | 41 | resource "docker_service" "service" { 42 | # ... 43 | secrets = [ 44 | { 45 | secret_id = "${docker_secret.service_secret.id}" 46 | secret_name = "${docker_secret.service_secret.name}" 47 | file_name = "/root/configs/configs.json" 48 | }, 49 | ] 50 | } 51 | ``` 52 | 53 | ## Argument Reference 54 | 55 | The following arguments are supported: 56 | 57 | * `name` - (Required, string) The name of the Docker secret. 58 | * `data` - (Required, string) The base64 encoded data of the secret. 59 | * `labels` - (Optional, block) See [Labels](#labels-1) below for details. 60 | 61 | 62 | #### Labels 63 | 64 | `labels` is a block within the configuration that can be repeated to specify 65 | additional label name and value data to the container. Each `labels` block supports 66 | the following: 67 | 68 | * `label` - (Required, string) Name of the label 69 | * `value` (Required, string) Value of the label 70 | 71 | See [214](https://github.com/terraform-providers/terraform-provider-docker/issues/214#issuecomment-550128950) for Details. 72 | 73 | ## Attributes Reference 74 | 75 | The following attributes are exported in addition to the above configuration: 76 | 77 | * `id` (string) 78 | 79 | ## Import 80 | 81 | Docker secret cannot be imported as the secret data, once set, is never exposed again. -------------------------------------------------------------------------------- /website/docs/r/volume.html.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docker" 3 | page_title: "Docker: docker_volume" 4 | sidebar_current: "docs-docker-resource-volume" 5 | description: |- 6 | Creates and destroys docker volumes. 7 | --- 8 | 9 | # docker\_volume 10 | 11 | Creates and destroys a volume in Docker. This can be used alongside 12 | [docker\_container](/docs/providers/docker/r/container.html) 13 | to prepare volumes that can be shared across containers. 14 | 15 | ## Example Usage 16 | 17 | ```hcl 18 | # Creates a docker volume "shared_volume". 19 | resource "docker_volume" "shared_volume" { 20 | name = "shared_volume" 21 | } 22 | 23 | # Reference the volume with ${docker_volume.shared_volume.name} 24 | 25 | ``` 26 | 27 | ## Argument Reference 28 | 29 | The following arguments are supported: 30 | 31 | * `name` - (Optional, string) The name of the Docker volume (generated if not 32 | provided). 33 | * `labels` - (Optional, map of string/string key/value pairs) User-defined key/value metadata. 34 | * `driver` - (Optional, string) Driver type for the volume (defaults to local). 35 | * `driver_opts` - (Optional, map of strings) Options specific to the driver. 36 | 37 | ## Attributes Reference 38 | 39 | The following attributes are exported in addition to the above configuration: 40 | 41 | * `mountpoint` (string) - The mountpoint of the volume. 42 | 43 | ## Import 44 | 45 | Docker volume can be imported using the long id, e.g. for a volume with the short id `ecae276c5`: 46 | 47 | ```sh 48 | $ terraform import docker_volume.foo $(docker volume inspect -f {{.ID}} eca) 49 | ``` --------------------------------------------------------------------------------