├── .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 | - [](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 | ```
--------------------------------------------------------------------------------