├── terraform-registry-manifest.json ├── tools └── tools.go ├── .github ├── CODE_OF_CONDUCT.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── docker-compose.yaml ├── examples ├── resources │ ├── podman_volume │ │ └── resource.tf │ ├── podman_pod │ │ └── resource.tf │ └── podman_network │ │ └── resource.tf ├── provider │ └── provider.tf └── README.md ├── .gitignore ├── internal ├── validators │ ├── validator.go │ ├── string.go │ ├── validator_test.go │ ├── network.go │ └── string_test.go ├── utils │ ├── diags.go │ └── maps.go ├── provider │ ├── provider_test.go │ ├── resource_volume_test.go │ ├── resource_volume.go │ ├── resource_pod_test.go │ ├── resource_network_crud.go │ ├── resource.go │ ├── resource_pod_crud.go │ ├── resource_volume_crud.go │ ├── resource_network_test.go │ ├── provider.go │ ├── resource_pod.go │ ├── resource_network.go │ └── shared │ │ ├── mount_options.go │ │ └── mount.go └── modifier │ ├── default.go │ ├── unknown_state.go │ └── replace_computed.go ├── GNUmakefile ├── main.go ├── docs ├── resources │ ├── volume.md │ ├── network.md │ └── pod.md └── index.md ├── README.md ├── .goreleaser.yml ├── go.mod └── LICENSE /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | // Documentation generation 7 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 8 | ) 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" # optional since v1.27.0 2 | services: 3 | podman: 4 | pull_policy: always 5 | image: ghcr.io/project0/podman-container:${PODMAN_VERSION:-latest} 6 | ports: 7 | - "10888:10888" 8 | cap_add: 9 | - sys_admin 10 | - mknod 11 | devices: 12 | - /dev/fuse 13 | privileged: true 14 | -------------------------------------------------------------------------------- /examples/resources/podman_volume/resource.tf: -------------------------------------------------------------------------------- 1 | # Create default local volume 2 | resource "podman_volume" "vol" {} 3 | 4 | # A local volume with mount options 5 | resource "podman_volume" "tmpfs" { 6 | name = "myvol" 7 | driver = "local" 8 | # driver specific options 9 | options = { 10 | # mount device 11 | device = "tmpfs" 12 | # mount -t 13 | type = "tmpfs" 14 | # mount options 15 | o = "nodev,noexec" 16 | } 17 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See GitHub's documentation for more information on this file: 2 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | podman = { 4 | source = "project0/podman" 5 | } 6 | } 7 | } 8 | 9 | # Per default connects to local unix socket 10 | provider "podman" { 11 | // default 12 | uri = "unix:///run/podman/podman.sock" 13 | } 14 | 15 | # connect via ssh 16 | provider "podman" { 17 | alias = "ssh" 18 | uri = "ssh://@[:port]/run/podman/podman.sock?secure=True" 19 | identity = "/tmp/ssh_identity_key" 20 | } 21 | -------------------------------------------------------------------------------- /examples/resources/podman_pod/resource.tf: -------------------------------------------------------------------------------- 1 | # Create default local volume 2 | resource "podman_volume" "vol" {} 3 | 4 | # A pod with mounts 5 | resource "podman_pod" "pod" { 6 | name = "mypod" 7 | hostname = "superhost" 8 | mounts = [ 9 | { 10 | destination = "/mount" 11 | bind = { 12 | path = "/mnt/local" 13 | chown = true 14 | } 15 | }, 16 | { 17 | destination = "/data" 18 | volume = { 19 | name = podman_volume.vol.name 20 | read_only = true 21 | } 22 | }, 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/resources/podman_network/resource.tf: -------------------------------------------------------------------------------- 1 | # Create default bridge network 2 | resource "podman_network" "network" {} 3 | 4 | # Full dual-stack example (netavark backend) 5 | resource "podman_network" "dualstack" { 6 | name = "dualstack" 7 | driver = "bridge" 8 | options = { 9 | mtu = 1500 10 | } 11 | internal = false 12 | dns = true 13 | # enable dual stack 14 | ipv6 = true 15 | subnets = [ 16 | { 17 | subnet = "2001:db8::/64" 18 | gateway = "2001:db8::1" 19 | }, 20 | { 21 | subnet = "192.0.2.0/24" 22 | gateway = "192.0.2.1" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | .DS_Store 4 | example.tf 5 | terraform.tfplan 6 | terraform.tfstate 7 | bin/ 8 | dist/ 9 | modules-dev/ 10 | /pkg/ 11 | website/.vagrant 12 | website/.bundle 13 | website/build 14 | website/node_modules 15 | .vagrant/ 16 | *.backup 17 | ./*.tfstate 18 | .terraform/ 19 | *.log 20 | *.bak 21 | *~ 22 | .*.swp 23 | .idea 24 | *.iml 25 | *.test 26 | *.iml 27 | 28 | website/vendor 29 | 30 | # Test exclusions 31 | !command/test-fixtures/**/*.tfstate 32 | !command/test-fixtures/**/.terraform/ 33 | 34 | # Keep windows files with windows line endings 35 | *.winfile eol=crlf 36 | 37 | terraform-provider-podman 38 | tmp/ -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. 4 | 5 | The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or ar testable even if some parts are not relevant for the documentation. 6 | 7 | * **provider/provider.tf** example file for the provider index page 8 | * **data-sources/`full data source name`/data-source.tf** example file for the named data source page 9 | * **resources/`full resource name`/resource.tf** example file for the named data source page 10 | -------------------------------------------------------------------------------- /internal/validators/validator.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 7 | ) 8 | 9 | type ( 10 | genericStringValidator struct { 11 | description string 12 | validate func(context.Context, validator.StringRequest, *validator.StringResponse) 13 | } 14 | ) 15 | 16 | func (v *genericStringValidator) Description(ctx context.Context) string { 17 | return v.description 18 | } 19 | 20 | func (v *genericStringValidator) MarkdownDescription(ctx context.Context) string { 21 | return v.Description(ctx) 22 | } 23 | 24 | func (v *genericStringValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 25 | if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { 26 | return 27 | } 28 | v.validate(ctx, req, resp) 29 | } 30 | -------------------------------------------------------------------------------- /internal/validators/string.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 7 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 8 | ) 9 | 10 | var ( 11 | regexName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) 12 | regexOctal = regexp.MustCompile(`^[0-7]{3,4}$`) 13 | regexTmpfSize = regexp.MustCompile(`^(\d+[kmg]?|\d{1,3}%)$`) 14 | ) 15 | 16 | // MatchName validates given name to be compatible with podman 17 | func MatchName() validator.String { 18 | return stringvalidator.RegexMatches(regexName, "") 19 | } 20 | 21 | // MatchOctal validates chmod octal representation 22 | func MatchOctal() validator.String { 23 | return stringvalidator.RegexMatches(regexOctal, "") 24 | } 25 | 26 | // MatchTmpfSize validates the tmpfs size option 27 | func MatchTmpfSize() validator.String { 28 | return stringvalidator.RegexMatches(regexTmpfSize, "") 29 | } 30 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | # https://github.com/containers/podman/issues/12548 2 | GOFLAGS ?= -tags=remote,exclude_graphdriver_btrfs,btrfs_noversion,exclude_graphdriver_devicemapper,containers_image_openpgp 3 | 4 | TEST_DOCKER_COMPOSE ?= tcp://localhost:10888 5 | TF_ACC_TEST_PROVIDER_PODMAN_URI ?= $(TEST_DOCKER_COMPOSE) 6 | 7 | export PODMAN_VERSION ?= latest 8 | 9 | default: testacc lint 10 | 11 | # Run acceptance tests (optional in rootless docker) 12 | .PHONY: testacc test 13 | testacc: 14 | ifeq ($(TEST_DOCKER_COMPOSE),$(TF_ACC_TEST_PROVIDER_PODMAN_URI)) 15 | docker-compose up -d 16 | endif 17 | TF_ACC_TEST_PROVIDER_PODMAN_URI=$(TF_ACC_TEST_PROVIDER_PODMAN_URI) TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m 18 | ifeq ($(TEST_DOCKER_COMPOSE),$(TF_ACC_TEST_PROVIDER_PODMAN_URI)) 19 | docker-compose down 20 | endif 21 | 22 | test: 23 | go test -v ./... 24 | 25 | .PHONY: build generate lint 26 | build: 27 | go build 28 | 29 | generate: 30 | go generate ./... 31 | 32 | lint: 33 | golangci-lint run -v 34 | -------------------------------------------------------------------------------- /internal/utils/diags.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/diag" 7 | "github.com/hashicorp/terraform-plugin-framework/path" 8 | ) 9 | 10 | const ( 11 | diagMsgErrorSummary = "Unexpected provider error: %s" 12 | diagMsgErrorDetail = "%s This is always a bug in the provider code and should be reported to the provider developers." 13 | ) 14 | 15 | // AddUnexpectedError adds a diagnostic error with injected bug hint 16 | func AddUnexpectedError(d *diag.Diagnostics, summary, detail string) { 17 | d.AddError( 18 | fmt.Sprintf(diagMsgErrorSummary, summary), 19 | fmt.Sprintf(diagMsgErrorDetail, detail), 20 | ) 21 | } 22 | 23 | // AddUnexpectedAttributeError adds a diagnostic error with injected bug hint 24 | func AddUnexpectedAttributeError(path path.Path, d *diag.Diagnostics, summary, detail string) { 25 | d.AddAttributeError( 26 | path, 27 | fmt.Sprintf(diagMsgErrorSummary, summary), 28 | fmt.Sprintf(diagMsgErrorDetail, detail), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /internal/validators/validator_test.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | type testValidatorStringCase struct { 12 | desc string 13 | values []types.String 14 | wantFail bool 15 | validator validator.String 16 | } 17 | 18 | func testValidatorStringExecute(t *testing.T, testCases []testValidatorStringCase) { 19 | for _, test := range testCases { 20 | t.Run(test.desc, func(t *testing.T) { 21 | 22 | for _, val := range test.values { 23 | req := validator.StringRequest{ 24 | ConfigValue: val, 25 | } 26 | resp := &validator.StringResponse{} 27 | 28 | test.validator.ValidateString(context.TODO(), req, resp) 29 | if test.wantFail != resp.Diagnostics.HasError() { 30 | t.Errorf("%s: value=%[2]q, err: %v", test.desc, val.ValueString(), resp.Diagnostics) 31 | } 32 | } 33 | 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 7 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 9 | ) 10 | 11 | // testAccProtoV6ProviderFactories are used to instantiate a provider during 12 | // acceptance testing. The factory function will be invoked for every Terraform 13 | // CLI command executed to create a provider server to which the CLI can 14 | // reattach. 15 | var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ 16 | "podman": providerserver.NewProtocol6WithError(New()), 17 | } 18 | 19 | // GenerateResourceName generates a random name with a constant prefix, useful for acceptance tests 20 | func generateResourceName() string { 21 | return "tf-testacc-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) 22 | } 23 | 24 | func testAccPreCheck(t *testing.T) { 25 | // You can add code here to run prior to any test case execution, for example assertions 26 | // about the appropriate environment variables being set are common to see in a pre-check 27 | // function. 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 9 | "github.com/project0/terraform-provider-podman/internal/provider" 10 | ) 11 | 12 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 13 | 14 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 15 | // ensure the documentation is formatted properly. 16 | //go:generate terraform fmt -recursive ./examples/ 17 | 18 | // Run the docs generation tool, check its repository for more information on how it works and how docs 19 | // can be customized. 20 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 21 | 22 | func main() { 23 | var debug bool 24 | 25 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 26 | flag.Parse() 27 | 28 | err := providerserver.Serve(context.Background(), provider.New, providerserver.ServeOpts{ 29 | Debug: debug, 30 | 31 | Address: "registry.terraform.io/project/podman", 32 | }) 33 | 34 | if err != nil { 35 | log.Fatal(err.Error()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/resources/volume.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "podman_volume Resource - terraform-provider-podman" 4 | subcategory: "" 5 | description: |- 6 | Manage volumes for containers and pods 7 | --- 8 | 9 | # podman_volume (Resource) 10 | 11 | Manage volumes for containers and pods 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | # Create default local volume 17 | resource "podman_volume" "vol" {} 18 | 19 | # A local volume with mount options 20 | resource "podman_volume" "tmpfs" { 21 | name = "myvol" 22 | driver = "local" 23 | # driver specific options 24 | options = { 25 | # mount device 26 | device = "tmpfs" 27 | # mount -t 28 | type = "tmpfs" 29 | # mount options 30 | o = "nodev,noexec" 31 | } 32 | } 33 | ``` 34 | 35 | 36 | ## Schema 37 | 38 | ### Optional 39 | 40 | - `driver` (String) Name of the volume driver. Defaults by podman to `local`. 41 | - `labels` (Map of String) Labels is a set of user defined key-value labels of the resource 42 | - `name` (String) Name of the resource, also used as ID. If not given a name will be automatically assigned. 43 | - `options` (Map of String) Driver specific options. 44 | 45 | ### Read-Only 46 | 47 | - `id` (String) ID of the resource 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "podman Provider" 4 | subcategory: "" 5 | description: |- 6 | The Podman provider provides resource management via the remote API. 7 | --- 8 | 9 | # podman Provider 10 | 11 | The Podman provider provides resource management via the remote API. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | terraform { 17 | required_providers { 18 | podman = { 19 | source = "project0/podman" 20 | } 21 | } 22 | } 23 | 24 | # Per default connects to local unix socket 25 | provider "podman" { 26 | // default 27 | uri = "unix:///run/podman/podman.sock" 28 | } 29 | 30 | # connect via ssh 31 | provider "podman" { 32 | alias = "ssh" 33 | uri = "ssh://@[:port]/run/podman/podman.sock?secure=True" 34 | identity = "/tmp/ssh_identity_key" 35 | } 36 | ``` 37 | 38 | 39 | ## Schema 40 | 41 | ### Optional 42 | 43 | - `identity` (String) Local path to the identity file for SSH based connections. 44 | - `uri` (String) Connection URI to the podman service. A valid URI connection should be of `scheme://`. For example `tcp://localhost:`or `unix:///run/podman/podman.sock`or `ssh://@[:port]/run/podman/podman.sock?secure=True`.Defaults to `unix:///run/podman/podman.sock`. 45 | -------------------------------------------------------------------------------- /internal/validators/network.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "regexp" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 10 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 11 | ) 12 | 13 | var ( 14 | regexNetworkInterface = regexp.MustCompile(`^[a-z][a-z0-9]*$`) 15 | ) 16 | 17 | func MatchNetworkInterfaceName() validator.String { 18 | return stringvalidator.RegexMatches(regexNetworkInterface, "") 19 | } 20 | 21 | func IsCIDR() validator.String { 22 | return &genericStringValidator{ 23 | description: "", 24 | validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 25 | _, _, err := net.ParseCIDR(req.ConfigValue.ValueString()) 26 | if err != nil { 27 | resp.Diagnostics.AddAttributeError( 28 | req.Path, 29 | "Failed to parse CIDR", 30 | fmt.Sprintf("invalid value: %s, error: %s", req.ConfigValue.String(), err.Error()), 31 | ) 32 | } 33 | }, 34 | } 35 | } 36 | 37 | func IsIpAdress() validator.String { 38 | return &genericStringValidator{ 39 | description: "", 40 | validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 41 | if net.ParseIP(req.ConfigValue.ValueString()) == nil { 42 | resp.Diagnostics.AddAttributeError( 43 | req.Path, 44 | "Failed to parse IP address", 45 | fmt.Sprintf("invalid value: %s", req.ConfigValue.String()), 46 | ) 47 | } 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/utils/maps.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/attr" 8 | "github.com/hashicorp/terraform-plugin-framework/diag" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | ) 11 | 12 | func MapStringEmpty() types.Map { 13 | m, _ := types.MapValue(types.StringType, make(map[string]attr.Value)) 14 | return m 15 | } 16 | 17 | // MapStringToMapType maps a native golang map to a terraform map type 18 | func MapStringToMapType(m map[string]string, diags *diag.Diagnostics) types.Map { 19 | elems := make(map[string]attr.Value) 20 | for k, v := range m { 21 | elems[k] = types.StringValue(v) 22 | } 23 | v, d := types.MapValue(types.StringType, elems) 24 | diags.Append(d...) 25 | return v 26 | } 27 | 28 | // MapStringValueToStringType extracts a terraform string value from a map 29 | func MapStringValueToStringType(m map[string]string, key string) types.String { 30 | if val, exist := m[key]; exist { 31 | return types.StringValue(val) 32 | } 33 | return types.StringNull() 34 | } 35 | 36 | // MapStringValueToIntType extracts a terraform int value from a map with string 37 | func MapStringValueToIntType(m map[string]string, key string, diags *diag.Diagnostics) types.Int64 { 38 | val, exist := m[key] 39 | i := 0 40 | if exist { 41 | var err error 42 | i, err = strconv.Atoi(val) 43 | if err != nil { 44 | diags.AddError("Cannot convert string to integer", fmt.Sprintf("Received value %s for key %s is not convertable: %s ", val, key, err.Error())) 45 | } 46 | return types.Int64Value(int64(i)) 47 | } 48 | return types.Int64Null() 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider for Podman 2 | 3 | [![Release](https://img.shields.io/github/v/release/project0/terraform-provider-podman)](https://github.com/project0/terraform-provider-podman/releases) 4 | [![Registry](https://img.shields.io/badge/registry-doc%40latest-lightgrey?logo=terraform)](https://registry.terraform.io/providers/project0/podman/latest/docs) 5 | [![License](https://img.shields.io/badge/license-Mozilla-blue.svg)](https://github.com/project0/terraform-provider-podman/blob/main/LICENSE) 6 | [![Build](https://github.com/project0/terraform-provider-podman/workflows/Tests/badge.svg)](https://github.com/project0/terraform-provider-podman/actions) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/project0/terraform-provider-podman)](https://goreportcard.com/report/github.com/project0/terraform-provider-podman) 8 | 9 | This terraform provider implements a resource management with [Podman (4)](https://podman.io/) remote API to leverage pod support. 10 | 11 | ## Development 12 | 13 | _This repository is built on the [Terraform Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework)._ 14 | 15 | ### Further information 16 | 17 | - [Plugin Development with the Terraform Plugin Framework](https://www.terraform.io/plugin/framework) 18 | - [Unit testing with SDKv2](https://www.terraform.io/plugin/sdkv2/testing) 19 | - [Documentation is auto generated by terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs/) 20 | 21 | ### Requirements 22 | 23 | - [Terraform](https://www.terraform.io/downloads.html) >= 1.0 24 | - [Go](https://golang.org/doc/install) >= 1.17 25 | - [Podman](https://podman.io/) >= 4.0 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Terraform Provider release workflow. 2 | name: Release 3 | 4 | # This GitHub action creates a release when a tag that matches the pattern 5 | # "v*" (e.g. v0.1.0) is created. 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | # Releases need permissions to read and write the repository contents. 12 | # GitHub considers creating releases and uploading assets as writing contents. 13 | permissions: 14 | contents: write 15 | 16 | # Default values to simplify job configurations below. 17 | env: 18 | # Go language version to use for building. This value should also be updated 19 | # in the testing workflow if changed. 20 | GO_VERSION: '1.19' 21 | 22 | jobs: 23 | goreleaser: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | # Allow goreleaser to access older tag information. 29 | fetch-depth: 0 30 | - uses: actions/setup-go@v3 31 | with: 32 | go-version: ${{ env.GO_VERSION }} 33 | # This uses an action (hashicorp/ghaction-import-gpg) that assumes you set your 34 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 35 | # secret. If you would rather own your own GPG handling, please fork this action 36 | # or use an alternative one for key handling. 37 | - name: Import GPG key 38 | id: import_gpg 39 | uses: hashicorp/ghaction-import-gpg@v2.1.0 40 | env: 41 | # These secrets will need to be configured for the repository: 42 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 43 | PASSPHRASE: ${{ secrets.PASSPHRASE }} 44 | - name: Run GoReleaser 45 | uses: goreleaser/goreleaser-action@v4.2.0 46 | with: 47 | args: release --rm-dist 48 | env: 49 | # GitHub sets the GITHUB_TOKEN secret automatically. 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 52 | -------------------------------------------------------------------------------- /internal/validators/string_test.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/types" 7 | ) 8 | 9 | func testStringToVals(str ...string) []types.String { 10 | vals := make([]types.String, len(str)) 11 | for i := 0; i < len(str); i++ { 12 | vals[i] = types.StringValue(str[i]) 13 | } 14 | return vals 15 | } 16 | 17 | func TestStringValidator_Octal(t *testing.T) { 18 | tests := []testValidatorStringCase{ 19 | { 20 | desc: "Null and Unknown is valid", 21 | values: []types.String{ 22 | types.StringUnknown(), 23 | types.StringNull(), 24 | }, 25 | validator: MatchOctal(), 26 | }, 27 | { 28 | desc: "Octal is valid", 29 | values: testStringToVals( 30 | "000", 31 | "644", 32 | "1755", 33 | "777", 34 | "600", 35 | "0400", 36 | "0777", 37 | ), 38 | validator: MatchOctal(), 39 | }, 40 | { 41 | desc: "Octal should fail", 42 | values: testStringToVals( 43 | "somestring", 44 | "1 2", 45 | "0", 46 | "12345", 47 | "9999", 48 | "66", 49 | "14444", 50 | "s644", 51 | ), 52 | wantFail: true, 53 | validator: MatchOctal(), 54 | }, 55 | } 56 | testValidatorStringExecute(t, tests) 57 | } 58 | 59 | func TestStringValidator_TmpfSize(t *testing.T) { 60 | tests := []testValidatorStringCase{ 61 | { 62 | desc: "Null and Unknown is valid", 63 | values: []types.String{ 64 | types.StringUnknown(), 65 | types.StringNull(), 66 | }, 67 | validator: MatchTmpfSize(), 68 | }, 69 | { 70 | desc: "TmpfSize is valid", 71 | values: testStringToVals( 72 | "4095", 73 | "666k", 74 | "200m", 75 | "100g", 76 | "1%", 77 | "20%", 78 | "90%", 79 | "100%", 80 | ), 81 | validator: MatchTmpfSize(), 82 | }, 83 | { 84 | desc: "TmpfSize should fail", 85 | values: testStringToVals( 86 | "somestring", 87 | "1 2", 88 | "100Gi", 89 | "2Mib", 90 | "m", 91 | "k", 92 | ), 93 | wantFail: true, 94 | validator: MatchTmpfSize(), 95 | }, 96 | } 97 | 98 | testValidatorStringExecute(t, tests) 99 | } 100 | -------------------------------------------------------------------------------- /docs/resources/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "podman_network Resource - terraform-provider-podman" 4 | subcategory: "" 5 | description: |- 6 | Manage networks for containers and pods 7 | --- 8 | 9 | # podman_network (Resource) 10 | 11 | Manage networks for containers and pods 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | # Create default bridge network 17 | resource "podman_network" "network" {} 18 | 19 | # Full dual-stack example (netavark backend) 20 | resource "podman_network" "dualstack" { 21 | name = "dualstack" 22 | driver = "bridge" 23 | options = { 24 | mtu = 1500 25 | } 26 | internal = false 27 | dns = true 28 | # enable dual stack 29 | ipv6 = true 30 | subnets = [ 31 | { 32 | subnet = "2001:db8::/64" 33 | gateway = "2001:db8::1" 34 | }, 35 | { 36 | subnet = "192.0.2.0/24" 37 | gateway = "192.0.2.1" 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | 44 | ## Schema 45 | 46 | ### Optional 47 | 48 | - `dns` (Boolean) Enable the DNS plugin for this network which if enabled, can perform container to container name resolution. Defaults to `false`. 49 | - `driver` (String) Driver to manage the network. One of `bridge`, `macvlan`, `ipvlan` are currently supported. By podman defaults to `bridge`. 50 | - `internal` (Boolean) Internal is whether the Network should not have external routes to public or other Networks. Defaults to `false`. 51 | - `ipam_driver` (String) Set the ipam driver (IP Address Management Driver) for the network. Valid values are `host-local`, `dhcp`, `none`. When unset podman will choose an ipam driver automatically based on the network driver. 52 | - `ipv6` (Boolean) Enable IPv6 (Dual Stack) networking. If no subnets are given it will allocate a ipv4 and ipv6 subnet. Defaults to `false`. 53 | - `labels` (Map of String) Labels is a set of user defined key-value labels of the resource 54 | - `name` (String) Name of the resource, also used as ID. If not given a name will be automatically assigned. 55 | - `options` (Map of String) Driver specific options. 56 | - `subnets` (Attributes Set) Subnets for this network. (see [below for nested schema](#nestedatt--subnets)) 57 | 58 | ### Read-Only 59 | 60 | - `id` (String) ID of the resource 61 | 62 | 63 | ### Nested Schema for `subnets` 64 | 65 | Required: 66 | 67 | - `subnet` (String) The subnet in CIDR notation. 68 | 69 | Optional: 70 | 71 | - `gateway` (String) Gateway IP for this Network. 72 | 73 | 74 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | before: 4 | hooks: 5 | # this is just an example and not a requirement for provider building/publishing 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: '{{ .CommitTimestamp }}' 14 | flags: 15 | - -trimpath 16 | - -tags 17 | - remote exclude_graphdriver_btrfs btrfs_noversion exclude_graphdriver_devicemapper containers_image_openpgp 18 | ldflags: 19 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 20 | goos: 21 | # - freebsd 22 | - windows 23 | - linux 24 | - darwin 25 | goarch: 26 | - amd64 27 | - '386' 28 | - arm 29 | - arm64 30 | ignore: 31 | - goos: darwin 32 | goarch: '386' 33 | binary: '{{ .ProjectName }}_v{{ .Version }}' 34 | archives: 35 | - format: zip 36 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 37 | checksum: 38 | extra_files: 39 | - glob: 'terraform-registry-manifest.json' 40 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 41 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 42 | algorithm: sha256 43 | signs: 44 | - artifacts: checksum 45 | args: 46 | # if you are using this in a GitHub action or some other automated pipeline, you 47 | # need to pass the batch flag to indicate its not interactive. 48 | - "--batch" 49 | - "--local-user" 50 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 51 | - "--output" 52 | - "${signature}" 53 | - "--detach-sign" 54 | - "${artifact}" 55 | 56 | changelog: 57 | sort: asc 58 | use: github 59 | filters: 60 | exclude: 61 | - '^docs:' 62 | - '^test:' 63 | - '^chore' 64 | - Merge pull request 65 | - Merge remote-tracking branch 66 | - Merge branch 67 | - go mod tidy 68 | groups: 69 | - title: 'New Features' 70 | regexp: "^.*feat[(\\w)]*:+.*$" 71 | order: 0 72 | - title: 'Bug fixes' 73 | regexp: "^.*fix[(\\w)]*:+.*$" 74 | order: 10 75 | - title: Other work 76 | order: 999 77 | 78 | release: 79 | # If you want to manually examine the release before its live, uncomment this line: 80 | # draft: true 81 | extra_files: 82 | - glob: 'terraform-registry-manifest.json' 83 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 84 | footer: | 85 | **Full Changelog**: https://github.com/project0/terraform-provider-podman/compare/{{ .PreviousTag }}...{{ .Tag }} -------------------------------------------------------------------------------- /internal/provider/resource_volume_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func TestAccResourceVolume_basic(t *testing.T) { 11 | name1 := generateResourceName() 12 | name2 := generateResourceName() 13 | 14 | resource.Test(t, resource.TestCase{ 15 | PreCheck: func() { testAccPreCheck(t) }, 16 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 17 | Steps: []resource.TestStep{ 18 | // Create and Read testing 19 | { 20 | Config: testAccResourceVolumeConfig(name1), 21 | Check: resource.ComposeAggregateTestCheckFunc( 22 | resource.TestCheckResourceAttr("podman_volume.test", "name", name1), 23 | ), 24 | }, 25 | // ImportState testing 26 | { 27 | ResourceName: "podman_volume.test", 28 | ImportState: true, 29 | ImportStateVerify: true, 30 | }, 31 | // Update and Read testing 32 | { 33 | Config: testAccResourceVolumeConfig(name2), 34 | Check: resource.ComposeAggregateTestCheckFunc( 35 | resource.TestCheckResourceAttr("podman_volume.test", "name", name2), 36 | ), 37 | }, 38 | // Delete testing automatically occurs in TestCase 39 | }, 40 | }) 41 | } 42 | 43 | func TestAccResourceVolume_local(t *testing.T) { 44 | name1 := generateResourceName() 45 | resource.Test(t, resource.TestCase{ 46 | PreCheck: func() { testAccPreCheck(t) }, 47 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 48 | Steps: []resource.TestStep{ 49 | // Create and Read testing 50 | { 51 | Config: testAccResourceVolumeConfigFull(name1, "local", "type", "tmpfs"), 52 | Check: resource.ComposeAggregateTestCheckFunc( 53 | resource.TestCheckResourceAttr("podman_volume.test", "driver", "local"), 54 | ), 55 | }, 56 | // ImportState testing 57 | { 58 | ResourceName: "podman_volume.test", 59 | ImportState: true, 60 | ImportStateVerify: true, 61 | }, 62 | // Update and Read testing 63 | { 64 | Config: testAccResourceVolumeConfigFull(name1, "local", "o", "noexec"), 65 | Check: resource.ComposeAggregateTestCheckFunc( 66 | resource.TestCheckResourceAttr("podman_volume.test", "driver", "local"), 67 | ), 68 | }, 69 | // Delete testing automatically occurs in TestCase 70 | }, 71 | }) 72 | } 73 | 74 | func testAccResourceVolumeConfig(name string) string { 75 | return fmt.Sprintf(` 76 | resource "podman_volume" "test" { 77 | name = %[1]q 78 | } 79 | `, name) 80 | } 81 | 82 | func testAccResourceVolumeConfigFull(name, driver, optkey, optvalue string) string { 83 | return fmt.Sprintf(` 84 | resource "podman_volume" "test" { 85 | name = %[1]q 86 | 87 | driver = %[1]q 88 | options = { 89 | %[2]q = %[3]q 90 | } 91 | } 92 | `, driver, optkey, optvalue) 93 | } 94 | -------------------------------------------------------------------------------- /internal/provider/resource_volume.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/containers/podman/v4/pkg/domain/entities" 7 | "github.com/hashicorp/terraform-plugin-framework/diag" 8 | "github.com/hashicorp/terraform-plugin-framework/resource" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 11 | "github.com/hashicorp/terraform-plugin-framework/types" 12 | "github.com/project0/terraform-provider-podman/internal/modifier" 13 | "github.com/project0/terraform-provider-podman/internal/utils" 14 | ) 15 | 16 | type ( 17 | volumeResource struct { 18 | genericResource 19 | } 20 | volumeResourceData struct { 21 | ID types.String `tfsdk:"id"` 22 | Name types.String `tfsdk:"name"` 23 | Labels types.Map `tfsdk:"labels"` 24 | 25 | Driver types.String `tfsdk:"driver"` 26 | Options types.Map `tfsdk:"options"` 27 | } 28 | ) 29 | 30 | // Ensure the implementation satisfies the expected interfaces. 31 | var ( 32 | _ resource.Resource = &volumeResource{} 33 | _ resource.ResourceWithConfigure = &volumeResource{} 34 | _ resource.ResourceWithImportState = &volumeResource{} 35 | ) 36 | 37 | // NewVolumeResource creates a new volume resource. 38 | func NewVolumeResource() resource.Resource { 39 | return &volumeResource{} 40 | } 41 | 42 | // Configure adds the provider configured client to the resource. 43 | func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 44 | r.genericResource.Configure(ctx, req, resp) 45 | } 46 | 47 | // Metadata returns the resource type name. 48 | func (r volumeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 49 | resp.TypeName = req.ProviderTypeName + "_volume" 50 | } 51 | 52 | // Schema returns the resource schema. 53 | func (r volumeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 54 | resp.Schema = schema.Schema{ 55 | Description: "Manage volumes for containers and pods", 56 | Attributes: withGenericAttributes( 57 | map[string]schema.Attribute{ 58 | "driver": schema.StringAttribute{ 59 | MarkdownDescription: "Name of the volume driver. Defaults by podman to `local`.", 60 | Required: false, 61 | Optional: true, 62 | Computed: true, 63 | PlanModifiers: []planmodifier.String{ 64 | modifier.RequiresReplaceComputed(), 65 | }, 66 | }, 67 | "options": schema.MapAttribute{ 68 | Description: "Driver specific options.", 69 | Required: false, 70 | Optional: true, 71 | Computed: true, 72 | ElementType: types.StringType, 73 | PlanModifiers: []planmodifier.Map{ 74 | modifier.UseDefaultModifier(utils.MapStringEmpty()), 75 | modifier.RequiresReplaceComputed(), 76 | }, 77 | }, 78 | }, 79 | ), 80 | } 81 | } 82 | 83 | func fromVolumeResponse(v *entities.VolumeConfigResponse, diags *diag.Diagnostics) *volumeResourceData { 84 | return &volumeResourceData{ 85 | // volumes do not have IDs, it wilbe mapped to the unique name 86 | ID: types.StringValue(v.Name), 87 | Name: types.StringValue(v.Name), 88 | Driver: types.StringValue(v.Driver), 89 | Labels: utils.MapStringToMapType(v.Labels, diags), 90 | Options: utils.MapStringToMapType(v.Options, diags), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/provider/resource_pod_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func TestAccResourcePod_basic(t *testing.T) { 11 | name1 := generateResourceName() 12 | 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 16 | Steps: []resource.TestStep{ 17 | // Create and Read testing 18 | { 19 | Config: testAccResourcePod(name1), 20 | Check: resource.ComposeAggregateTestCheckFunc( 21 | resource.TestCheckResourceAttr("podman_pod.test", "name", name1), 22 | ), 23 | }, 24 | // ImportState testing 25 | { 26 | ResourceName: "podman_pod.test", 27 | ImportState: true, 28 | ImportStateVerify: true, 29 | }, 30 | // Update and Read testing 31 | { 32 | Config: testAccResourcePod(name1), 33 | Check: resource.ComposeAggregateTestCheckFunc( 34 | resource.TestCheckResourceAttr("podman_pod.test", "name", name1), 35 | ), 36 | }, 37 | // Delete testing automatically occurs in TestCase 38 | }, 39 | }) 40 | } 41 | 42 | func TestAccResourcePod_volume(t *testing.T) { 43 | 44 | name1 := generateResourceName() 45 | name2 := generateResourceName() 46 | 47 | resource.Test(t, resource.TestCase{ 48 | PreCheck: func() { testAccPreCheck(t) }, 49 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 50 | Steps: []resource.TestStep{ 51 | // Create and Read testing 52 | { 53 | Config: testAccResourcePodVolume(name1), 54 | Check: resource.ComposeAggregateTestCheckFunc( 55 | resource.TestCheckResourceAttr("podman_pod.test", "name", name1), 56 | ), 57 | }, 58 | // ImportState testing 59 | { 60 | ResourceName: "podman_pod.test", 61 | ImportState: true, 62 | ImportStateVerify: true, 63 | }, 64 | // Update and Read testing 65 | { 66 | Config: testAccResourcePodVolume(name1), 67 | Check: resource.ComposeAggregateTestCheckFunc( 68 | resource.TestCheckResourceAttr("podman_pod.test", "name", name1), 69 | ), 70 | }, 71 | 72 | // Test replace 73 | { 74 | Config: testAccResourcePodVolume(name2), 75 | Check: resource.ComposeAggregateTestCheckFunc( 76 | resource.TestCheckResourceAttr("podman_pod.test", "name", name2), 77 | ), 78 | }, 79 | // Delete testing automatically occurs in TestCase 80 | }, 81 | }) 82 | } 83 | 84 | func testAccResourcePod(configurableAttribute string) string { 85 | return fmt.Sprintf(` 86 | resource "podman_pod" "test" { 87 | name = %[1]q 88 | } 89 | `, configurableAttribute) 90 | } 91 | 92 | func testAccResourcePodVolume(name string) string { 93 | return fmt.Sprintf(` 94 | resource "podman_volume" "test" { 95 | name = %[1]q 96 | } 97 | 98 | resource "podman_pod" "test" { 99 | name = %[1]q 100 | mounts = [ 101 | { 102 | destination = "/data/one" 103 | volume = { 104 | name = podman_volume.test.name 105 | } 106 | }, 107 | { 108 | destination = "/data/two" 109 | volume = { 110 | name = podman_volume.test.name 111 | read_only = true 112 | exec = true 113 | suid = false 114 | chown = false 115 | idmap = false 116 | dev = false 117 | } 118 | } 119 | ] 120 | } 121 | `, name) 122 | } 123 | -------------------------------------------------------------------------------- /internal/provider/resource_network_crud.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/containers/podman/v4/pkg/bindings/network" 8 | 9 | "github.com/hashicorp/terraform-plugin-framework/path" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/project0/terraform-provider-podman/internal/utils" 12 | ) 13 | 14 | func (r networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 15 | var data networkResourceData 16 | 17 | client := r.initClientData(ctx, &data, req.Config.Get, &resp.Diagnostics) 18 | if resp.Diagnostics.HasError() { 19 | return 20 | } 21 | 22 | networkCreate := toPodmanNetwork(ctx, data, &resp.Diagnostics) 23 | if resp.Diagnostics.HasError() { 24 | return 25 | } 26 | 27 | networkResponse, err := network.Create(client, networkCreate) 28 | if err != nil { 29 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to create network resource: %s", err.Error())) 30 | return 31 | } 32 | 33 | diags := resp.State.Set(ctx, fromPodmanNetwork(networkResponse, &resp.Diagnostics)) 34 | resp.Diagnostics.Append(diags...) 35 | } 36 | 37 | func (r networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 38 | var data networkResourceData 39 | 40 | client := r.initClientData(ctx, &data, req.State.Get, &resp.Diagnostics) 41 | if resp.Diagnostics.HasError() { 42 | return 43 | } 44 | 45 | if exist, err := network.Exists(client, data.ID.ValueString(), nil); err != nil { 46 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read network resource: %s", err.Error())) 47 | return 48 | } else if !exist { 49 | resp.State.RemoveResource(ctx) 50 | return 51 | } 52 | 53 | networkResponse, err := network.Inspect(client, data.ID.ValueString(), nil) 54 | if err != nil { 55 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read network resource: %s", err.Error())) 56 | return 57 | } 58 | 59 | diags := resp.State.Set(ctx, fromPodmanNetwork(networkResponse, &resp.Diagnostics)) 60 | resp.Diagnostics.Append(diags...) 61 | } 62 | 63 | // Update is not implemented 64 | func (r networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 65 | utils.AddUnexpectedError( 66 | &resp.Diagnostics, 67 | "Update triggered for a network resource", 68 | "Networks are immutable resources and cannot be updated, it always needs to be replaced.", 69 | ) 70 | } 71 | 72 | func (r networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 73 | var data networkResourceData 74 | 75 | client := r.initClientData(ctx, &data, req.State.Get, &resp.Diagnostics) 76 | if resp.Diagnostics.HasError() { 77 | return 78 | } 79 | 80 | // TODO: Allow force which detaches containers from network? 81 | rmErrors, err := network.Remove(client, data.ID.ValueString(), nil) 82 | if err != nil { 83 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to delete network resource: %s", err.Error())) 84 | } 85 | for _, e := range rmErrors { 86 | if e.Err != nil { 87 | resp.Diagnostics.AddError("Error report on deletion for "+e.Name, e.Err.Error()) 88 | } 89 | } 90 | 91 | resp.State.RemoveResource(ctx) 92 | } 93 | 94 | func (r networkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 95 | resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) 96 | } 97 | -------------------------------------------------------------------------------- /internal/modifier/default.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/attr" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | // UseDefaultModifier returns a new DefaultModifier 12 | func UseDefaultModifier(value attr.Value) DefaultModifier { 13 | return DefaultModifier{ 14 | value: value, 15 | } 16 | } 17 | 18 | // DefaultModifier ensures a new value with null value will always be set to default 19 | type DefaultModifier struct { 20 | value attr.Value 21 | } 22 | 23 | func (m DefaultModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { 24 | if req.PlanValue.IsNull() { 25 | resp.PlanValue = m.value.(types.Bool) 26 | } 27 | } 28 | 29 | func (m DefaultModifier) PlanModifyFloat64(_ context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { 30 | if req.PlanValue.IsNull() { 31 | resp.PlanValue = m.value.(types.Float64) 32 | } 33 | } 34 | 35 | func (m DefaultModifier) PlanModifyInt64(_ context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { 36 | if req.PlanValue.IsNull() { 37 | resp.PlanValue = m.value.(types.Int64) 38 | } 39 | } 40 | 41 | func (m DefaultModifier) PlanModifyList(_ context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { 42 | if req.PlanValue.IsNull() { 43 | resp.PlanValue = m.value.(types.List) 44 | } 45 | } 46 | func (m DefaultModifier) PlanModifyMap(_ context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { 47 | if req.PlanValue.IsNull() { 48 | resp.PlanValue = m.value.(types.Map) 49 | } 50 | } 51 | 52 | func (m DefaultModifier) PlanModifyNumber(_ context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { 53 | if req.PlanValue.IsNull() { 54 | resp.PlanValue = m.value.(types.Number) 55 | } 56 | } 57 | 58 | func (m DefaultModifier) PlanModifyObject(_ context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { 59 | if req.PlanValue.IsNull() { 60 | resp.PlanValue = m.value.(types.Object) 61 | } 62 | } 63 | 64 | func (m DefaultModifier) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { 65 | if req.PlanValue.IsNull() { 66 | resp.PlanValue = m.value.(types.Set) 67 | } 68 | } 69 | 70 | func (m DefaultModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { 71 | if req.PlanValue.IsNull() { 72 | resp.PlanValue = m.value.(types.String) 73 | } 74 | } 75 | 76 | // Modify sets the default value if not known or empty 77 | //func (m DefaultModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { 78 | // val, err := req.AttributePlan.ToTerraformValue(ctx) 79 | // if err != nil { 80 | // utils.AddUnexpectedAttributeError(meq.AttributePath, &resp.Diagnostics, "Failed to retrieve value", err.Error()) 81 | // return 82 | // } 83 | // 84 | // if val.IsNull() { 85 | // resp.AttributePlan = r.value 86 | // } 87 | //} 88 | 89 | // Description returns a human-readable description of the plan modifier. 90 | func (m DefaultModifier) Description(_ context.Context) string { 91 | return "Ensure null values are replaced by given default attribute." 92 | } 93 | 94 | // MarkdownDescription returns a markdown description of the plan modifier. 95 | func (m DefaultModifier) MarkdownDescription(ctx context.Context) string { 96 | return m.Description(ctx) 97 | } 98 | -------------------------------------------------------------------------------- /internal/provider/resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/diag" 8 | "github.com/hashicorp/terraform-plugin-framework/resource" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | 16 | "github.com/hashicorp/terraform-plugin-log/tflog" 17 | "github.com/project0/terraform-provider-podman/internal/modifier" 18 | "github.com/project0/terraform-provider-podman/internal/utils" 19 | "github.com/project0/terraform-provider-podman/internal/validators" 20 | ) 21 | 22 | type ( 23 | genericResource struct { 24 | providerData providerData 25 | } 26 | ) 27 | 28 | // Configures the podman client 29 | func (g *genericResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 30 | 31 | // It looks like this method is called twice, the first time its nil and happen before the provider is initialized. 32 | if req.ProviderData == nil { 33 | return 34 | } 35 | 36 | var ok bool 37 | g.providerData, ok = req.ProviderData.(providerData) 38 | 39 | if !ok { 40 | utils.AddUnexpectedError( 41 | &resp.Diagnostics, 42 | "Provider Instance Type", 43 | fmt.Sprintf("While creating the data source or resource, an unexpected provider type (%T) was received.", req.ProviderData), 44 | ) 45 | } 46 | } 47 | 48 | func (g genericResource) initClientData( 49 | ctx context.Context, 50 | data interface{}, 51 | get func(context.Context, interface{}) diag.Diagnostics, 52 | diags *diag.Diagnostics, 53 | ) context.Context { 54 | 55 | diags.Append( 56 | get(ctx, data)..., 57 | ) 58 | 59 | if diags.HasError() { 60 | tflog.Error(ctx, "Failed to retrieve resource data") 61 | return nil 62 | } 63 | 64 | return newPodmanClient(ctx, diags, g.providerData) 65 | } 66 | 67 | // withGenericAttributes returns re-usable standard type definitions 68 | func withGenericAttributes(attributes map[string]schema.Attribute) map[string]schema.Attribute { 69 | // Name is also used as unique id in podman, 70 | // IDs itself only exists for docker compatibility and therefore does not make sense to implement 71 | attributes["name"] = schema.StringAttribute{ 72 | Description: "Name of the resource, also used as ID. If not given a name will be automatically assigned.", 73 | Required: false, 74 | Optional: true, 75 | Computed: true, 76 | Validators: []validator.String{ 77 | validators.MatchName(), 78 | }, 79 | PlanModifiers: []planmodifier.String{ 80 | stringplanmodifier.UseStateForUnknown(), 81 | modifier.RequiresReplaceComputed(), 82 | }, 83 | } 84 | 85 | attributes["labels"] = schema.MapAttribute{ 86 | Description: "Labels is a set of user defined key-value labels of the resource", 87 | Required: false, 88 | Optional: true, 89 | Computed: true, 90 | ElementType: types.StringType, 91 | PlanModifiers: []planmodifier.Map{ 92 | modifier.UseDefaultModifier(utils.MapStringEmpty()), 93 | mapplanmodifier.UseStateForUnknown(), 94 | modifier.RequiresReplaceComputed(), 95 | }, 96 | } 97 | 98 | attributes["id"] = schema.StringAttribute{ 99 | Description: "ID of the resource", 100 | Computed: true, 101 | PlanModifiers: []planmodifier.String{ 102 | stringplanmodifier.UseStateForUnknown(), 103 | // Podman (go bindingds only?) looks up resource by name or id so it may not trigger replace. 104 | modifier.RequiresReplaceComputed(), 105 | }, 106 | } 107 | 108 | return attributes 109 | } 110 | -------------------------------------------------------------------------------- /internal/provider/resource_pod_crud.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/containers/podman/v4/pkg/bindings/pods" 9 | "github.com/containers/podman/v4/pkg/domain/entities" 10 | "github.com/hashicorp/terraform-plugin-framework/path" 11 | "github.com/hashicorp/terraform-plugin-framework/resource" 12 | "github.com/hashicorp/terraform-plugin-log/tflog" 13 | "github.com/project0/terraform-provider-podman/internal/utils" 14 | ) 15 | 16 | func (r podResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 17 | var data podResourceData 18 | 19 | client := r.initClientData(ctx, &data, req.Config.Get, &resp.Diagnostics) 20 | if resp.Diagnostics.HasError() { 21 | return 22 | } 23 | 24 | podSpec := toPodmanPodSpecGenerator(ctx, data, &resp.Diagnostics) 25 | if resp.Diagnostics.HasError() { 26 | return 27 | } 28 | 29 | // Create 30 | podCreateResponse, errCreate := pods.CreatePodFromSpec(client, &entities.PodSpec{ 31 | PodSpecGen: *podSpec, 32 | }) 33 | if errCreate != nil { 34 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to create pod resource: %s", errCreate.Error())) 35 | return 36 | } 37 | 38 | podResponse, err := pods.Inspect(client, podCreateResponse.Id, nil) 39 | if err != nil { 40 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read pod resource after creation: %s", err.Error())) 41 | return 42 | } 43 | m, _ := json.MarshalIndent(podResponse, "", " ") 44 | tflog.Info(ctx, "read pod: %v", map[string]interface{}{"response": m}) 45 | 46 | // Set state 47 | resp.Diagnostics.Append( 48 | resp.State.Set(ctx, fromPodResponse(podResponse, &resp.Diagnostics))..., 49 | ) 50 | } 51 | 52 | func (r podResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 53 | var data podResourceData 54 | 55 | client := r.initClientData(ctx, &data, req.State.Get, &resp.Diagnostics) 56 | if resp.Diagnostics.HasError() { 57 | return 58 | } 59 | 60 | if exist, err := pods.Exists(client, data.ID.ValueString(), nil); err != nil { 61 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read (exists) pod resource: %s", err.Error())) 62 | return 63 | } else if !exist { 64 | resp.State.RemoveResource(ctx) 65 | return 66 | } 67 | 68 | podResponse, err := pods.Inspect(client, data.ID.ValueString(), nil) 69 | if err != nil { 70 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read (inspect) pod resource: %s", err.Error())) 71 | return 72 | } 73 | 74 | // Set state 75 | resp.Diagnostics.Append( 76 | resp.State.Set(ctx, fromPodResponse(podResponse, &resp.Diagnostics))..., 77 | ) 78 | } 79 | 80 | // Update is not implemented 81 | func (r podResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 82 | utils.AddUnexpectedError( 83 | &resp.Diagnostics, 84 | "Update triggered for a pod resource", 85 | "pods are immutable resources and cannot be updated, it always needs to be replaced.", 86 | ) 87 | } 88 | 89 | func (r podResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 90 | var data podResourceData 91 | 92 | client := r.initClientData(ctx, &data, req.State.Get, &resp.Diagnostics) 93 | if resp.Diagnostics.HasError() { 94 | return 95 | } 96 | 97 | // TODO: Allow force ? 98 | // TODO: handle report messages 99 | _, err := pods.Remove(client, data.ID.ValueString(), nil) 100 | if err != nil { 101 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to delete pod resource: %s", err.Error())) 102 | } 103 | 104 | resp.State.RemoveResource(ctx) 105 | } 106 | 107 | func (r podResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 108 | resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) 109 | } 110 | -------------------------------------------------------------------------------- /internal/provider/resource_volume_crud.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/containers/podman/v4/pkg/bindings/volumes" 8 | "github.com/containers/podman/v4/pkg/domain/entities" 9 | "github.com/hashicorp/terraform-plugin-framework/path" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/project0/terraform-provider-podman/internal/utils" 12 | ) 13 | 14 | func (r volumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 15 | var data volumeResourceData 16 | 17 | client := r.initClientData(ctx, &data, req.Config.Get, &resp.Diagnostics) 18 | if resp.Diagnostics.HasError() { 19 | return 20 | } 21 | 22 | // Build volume 23 | var volCreate = &entities.VolumeCreateOptions{ 24 | // null values are automatically empty string 25 | // as we do not use pointer we do not need to distinguish and pass it directly 26 | Name: data.Name.ValueString(), 27 | Driver: data.Driver.ValueString(), 28 | } 29 | 30 | resp.Diagnostics.Append(data.Labels.ElementsAs(ctx, &volCreate.Labels, false)...) 31 | if resp.Diagnostics.HasError() { 32 | return 33 | } 34 | 35 | // optional 36 | if !data.Options.IsNull() { 37 | resp.Diagnostics.Append(data.Options.ElementsAs(ctx, &volCreate.Options, true)...) 38 | } 39 | 40 | if resp.Diagnostics.HasError() { 41 | return 42 | } 43 | 44 | // Create 45 | volResponse, err := volumes.Create(client, *volCreate, nil) 46 | if err != nil { 47 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to create volume resource: %s", err.Error())) 48 | return 49 | } 50 | 51 | // Set state 52 | resp.Diagnostics.Append( 53 | resp.State.Set(ctx, fromVolumeResponse(volResponse, &resp.Diagnostics))..., 54 | ) 55 | } 56 | 57 | func (r volumeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 58 | var data volumeResourceData 59 | 60 | client := r.initClientData(ctx, &data, req.State.Get, &resp.Diagnostics) 61 | if resp.Diagnostics.HasError() { 62 | return 63 | } 64 | 65 | if exist, err := volumes.Exists(client, data.ID.ValueString(), nil); err != nil { 66 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read volume resource: %s", err.Error())) 67 | return 68 | } else if !exist { 69 | resp.State.RemoveResource(ctx) 70 | return 71 | } 72 | 73 | volResponse, err := volumes.Inspect(client, data.ID.ValueString(), nil) 74 | if err != nil { 75 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to read volume resource: %s", err.Error())) 76 | return 77 | } 78 | 79 | // Set state 80 | resp.Diagnostics.Append( 81 | resp.State.Set(ctx, fromVolumeResponse(volResponse, &resp.Diagnostics))..., 82 | ) 83 | } 84 | 85 | // Update is not implemented 86 | func (r volumeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 87 | utils.AddUnexpectedError( 88 | &resp.Diagnostics, 89 | "Update triggered for a volume resource", 90 | "Volumes are immutable resources and cannot be updated, it always needs to be replaced.", 91 | ) 92 | } 93 | 94 | func (r volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 95 | var data volumeResourceData 96 | 97 | client := r.initClientData(ctx, &data, req.State.Get, &resp.Diagnostics) 98 | if resp.Diagnostics.HasError() { 99 | return 100 | } 101 | 102 | // TODO: Allow force ? 103 | err := volumes.Remove(client, data.ID.ValueString(), nil) 104 | if err != nil { 105 | resp.Diagnostics.AddError("Podman client error", fmt.Sprintf("Failed to delete volume resource: %s", err.Error())) 106 | } 107 | 108 | resp.State.RemoveResource(ctx) 109 | } 110 | 111 | func (r volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 112 | resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) 113 | } 114 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Terraform Provider testing workflow. 2 | name: Tests 3 | 4 | # This GitHub action runs your tests for each pull request and push. 5 | # Optionally, you can turn it on using a schedule for regular testing. 6 | on: 7 | pull_request: 8 | paths-ignore: 9 | - 'README.md' 10 | push: 11 | paths-ignore: 12 | - 'README.md' 13 | 14 | # Testing only needs permissions to read the repository contents. 15 | permissions: 16 | contents: read 17 | 18 | # Default values to simplify job configurations below. 19 | env: 20 | # Go language version to use for building. This value should also be updated 21 | # in the release workflow if changed. 22 | GO_VERSION: '1.19' 23 | # https://github.com/containers/podman/issues/12548 24 | GOFLAGS: '-tags=remote,exclude_graphdriver_btrfs,btrfs_noversion,exclude_graphdriver_devicemapper,containers_image_openpgp' 25 | 26 | jobs: 27 | # Ensure project builds before running testing matrix 28 | build: 29 | name: Build 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 5 32 | steps: 33 | - uses: actions/setup-go@v3 34 | with: 35 | go-version: ${{ env.GO_VERSION }} 36 | - uses: actions/checkout@v3 37 | - uses: actions/cache@v3 38 | with: 39 | path: | 40 | ~/.cache/go-build 41 | ~/go/pkg/mod 42 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 43 | restore-keys: | 44 | ${{ runner.os }}-go- 45 | - if: steps.cache.outputs.cache-hit != 'true' 46 | run: go mod download 47 | - run: go build -v . 48 | - run: go test -v -cover ./... 49 | 50 | generate: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/setup-go@v3 54 | with: 55 | go-version: ${{ env.GO_VERSION }} 56 | - uses: actions/checkout@v3 57 | - uses: actions/cache@v3 58 | with: 59 | path: | 60 | ~/.cache/go-build 61 | ~/go/pkg/mod 62 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 63 | restore-keys: | 64 | ${{ runner.os }}-go- 65 | - if: steps.cache.outputs.cache-hit != 'true' 66 | run: go mod download 67 | - run: go generate ./... 68 | - name: git diff 69 | run: | 70 | git diff --compact-summary --exit-code || \ 71 | (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) 72 | 73 | # Run acceptance tests in a matrix with Terraform CLI versions 74 | test: 75 | name: Terraform Provider Acceptance Tests 76 | needs: build 77 | runs-on: ubuntu-latest 78 | timeout-minutes: 15 79 | strategy: 80 | fail-fast: false 81 | matrix: 82 | # list whatever Terraform versions here you would like to support 83 | terraform: 84 | - '1.0.*' 85 | - '1.1.*' 86 | - '1.2.*' 87 | - '1.3.*' 88 | # needs to exist in https://github.com/project0/podman-container 89 | podman: 90 | - '4.0' 91 | - '4.1' 92 | - '4.2' 93 | - '4.3' 94 | steps: 95 | - uses: actions/setup-go@v3 96 | with: 97 | go-version: ${{ env.GO_VERSION }} 98 | - uses: hashicorp/setup-terraform@v2 99 | with: 100 | terraform_version: ${{ matrix.terraform }} 101 | terraform_wrapper: false 102 | - uses: actions/checkout@v3 103 | - name: Build and execute podman daemon 104 | env: 105 | PODMAN_VERSION: ${{ matrix.podman }} 106 | run: docker-compose up -d 107 | - uses: actions/cache@v3 108 | with: 109 | path: | 110 | ~/.cache/go-build 111 | ~/go/pkg/mod 112 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 113 | restore-keys: | 114 | ${{ runner.os }}-go- 115 | - if: steps.cache.outputs.cache-hit != 'true' 116 | run: go mod download 117 | - env: 118 | TF_ACC_TEST_PROVIDER_PODMAN_URI: tcp://localhost:10888 119 | TF_ACC: "1" 120 | run: go test -v -cover ./internal/provider/ 121 | timeout-minutes: 10 122 | -------------------------------------------------------------------------------- /internal/provider/resource_network_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func TestAccResourceNetwork_basic(t *testing.T) { 11 | name1 := generateResourceName() 12 | name2 := generateResourceName() 13 | 14 | resource.Test(t, resource.TestCase{ 15 | PreCheck: func() { testAccPreCheck(t) }, 16 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 17 | Steps: []resource.TestStep{ 18 | // Create and Read testing 19 | { 20 | Config: testAccResourceNetwork(name1), 21 | Check: resource.ComposeAggregateTestCheckFunc( 22 | resource.TestCheckResourceAttr("podman_network.test", "name", name1), 23 | resource.TestCheckResourceAttr("podman_network.test", "driver", "bridge"), 24 | resource.TestCheckResourceAttr("podman_network.test", "internal", "false"), 25 | resource.TestCheckResourceAttr("podman_network.test", "dns", "false"), 26 | ), 27 | }, 28 | // ImportState testing 29 | { 30 | ResourceName: "podman_network.test", 31 | ImportState: true, 32 | ImportStateVerify: true, 33 | }, 34 | // Update and Read testing 35 | { 36 | Config: testAccResourceNetwork(name2), 37 | Check: resource.ComposeAggregateTestCheckFunc( 38 | resource.TestCheckResourceAttr("podman_network.test", "name", name2), 39 | resource.TestCheckResourceAttr("podman_network.test", "driver", "bridge"), 40 | ), 41 | }, 42 | // Delete testing automatically occurs in TestCase 43 | }, 44 | }) 45 | } 46 | 47 | func TestAccResourceNetwork_dualStack(t *testing.T) { 48 | name1 := generateResourceName() 49 | 50 | resource.Test(t, resource.TestCase{ 51 | PreCheck: func() { testAccPreCheck(t) }, 52 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 53 | Steps: []resource.TestStep{ 54 | // Create and Read testing 55 | { 56 | Config: testAccResourceNetworkDualStack(name1), 57 | Check: resource.ComposeAggregateTestCheckFunc( 58 | resource.TestCheckResourceAttr("podman_network.dualstack", "name", name1), 59 | resource.TestCheckResourceAttr("podman_network.dualstack", "driver", "bridge"), 60 | resource.TestCheckResourceAttr("podman_network.dualstack", "internal", "false"), 61 | resource.TestCheckResourceAttr("podman_network.dualstack", "dns", "true"), 62 | resource.TestCheckResourceAttr("podman_network.dualstack", "ipv6", "true"), 63 | resource.TestCheckTypeSetElemNestedAttrs("podman_network.dualstack", "subnets.*", 64 | map[string]string{ 65 | "subnet": "2001:db8::/64", 66 | "gateway": "2001:db8::1", 67 | }, 68 | ), 69 | resource.TestCheckTypeSetElemNestedAttrs("podman_network.dualstack", "subnets.*", 70 | map[string]string{ 71 | "subnet": "192.0.2.0/24", 72 | "gateway": "192.0.2.1", 73 | }, 74 | ), 75 | ), 76 | }, 77 | // ImportState testing 78 | { 79 | ResourceName: "podman_network.dualstack", 80 | ImportState: true, 81 | ImportStateVerify: true, 82 | }, 83 | // Update and Read testing 84 | { 85 | Config: testAccResourceNetworkDualStack(name1), 86 | Check: resource.ComposeAggregateTestCheckFunc( 87 | resource.TestCheckResourceAttr("podman_network.dualstack", "name", name1), 88 | ), 89 | }, 90 | // Delete testing automatically occurs in TestCase 91 | }, 92 | }) 93 | } 94 | 95 | func testAccResourceNetwork(name string) string { 96 | return fmt.Sprintf(` 97 | resource "podman_network" "test" { 98 | name = %[1]q 99 | } 100 | `, name) 101 | } 102 | 103 | func testAccResourceNetworkDualStack(name string) string { 104 | return fmt.Sprintf(` 105 | resource "podman_network" "dualstack" { 106 | name = %[1]q 107 | driver = "bridge" 108 | ipam_driver = "host-local" 109 | options = { 110 | mtu = 1500 111 | } 112 | internal = false 113 | dns = true 114 | # enable dual stack 115 | ipv6 = true 116 | subnets = [ 117 | { 118 | subnet = "2001:db8::/64" 119 | gateway = "2001:db8::1" 120 | }, 121 | { 122 | subnet = "192.0.2.0/24" 123 | gateway = "192.0.2.1" 124 | } 125 | ] 126 | } 127 | `, name) 128 | } 129 | -------------------------------------------------------------------------------- /internal/modifier/unknown_state.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/attr" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/tfsdk" 9 | ) 10 | 11 | func AlwaysUseStateForUnknown() AlwaysUseStateForUnknownModifier { 12 | return AlwaysUseStateForUnknownModifier{} 13 | } 14 | 15 | type AlwaysUseStateForUnknownModifier struct{} 16 | 17 | func (m AlwaysUseStateForUnknownModifier) Description(_ context.Context) string { 18 | return "" 19 | } 20 | 21 | func (m AlwaysUseStateForUnknownModifier) MarkdownDescription(_ context.Context) string { 22 | return "" 23 | } 24 | 25 | func (m AlwaysUseStateForUnknownModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { 26 | if m.setState(req.State, req.Plan, resp.PlanValue) { 27 | resp.PlanValue = req.StateValue 28 | } 29 | } 30 | 31 | func (m AlwaysUseStateForUnknownModifier) PlanModifyFloat64(_ context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { 32 | if m.setState(req.State, req.Plan, resp.PlanValue) { 33 | resp.PlanValue = req.StateValue 34 | } 35 | } 36 | 37 | func (m AlwaysUseStateForUnknownModifier) PlanModifyInt64(_ context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { 38 | if m.setState(req.State, req.Plan, resp.PlanValue) { 39 | resp.PlanValue = req.StateValue 40 | } 41 | } 42 | 43 | func (m AlwaysUseStateForUnknownModifier) PlanModifyList(_ context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { 44 | if m.setState(req.State, req.Plan, resp.PlanValue) { 45 | resp.PlanValue = req.StateValue 46 | } 47 | } 48 | 49 | func (m AlwaysUseStateForUnknownModifier) PlanModifyMap(_ context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { 50 | if m.setState(req.State, req.Plan, resp.PlanValue) { 51 | resp.PlanValue = req.StateValue 52 | } 53 | } 54 | 55 | func (m AlwaysUseStateForUnknownModifier) PlanModifyNumber(_ context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { 56 | if m.setState(req.State, req.Plan, resp.PlanValue) { 57 | resp.PlanValue = req.StateValue 58 | } 59 | } 60 | 61 | func (m AlwaysUseStateForUnknownModifier) PlanModifyObject(_ context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { 62 | if m.setState(req.State, req.Plan, resp.PlanValue) { 63 | resp.PlanValue = req.StateValue 64 | } 65 | } 66 | 67 | func (m AlwaysUseStateForUnknownModifier) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { 68 | if m.setState(req.State, req.Plan, resp.PlanValue) { 69 | resp.PlanValue = req.StateValue 70 | } 71 | } 72 | 73 | func (m AlwaysUseStateForUnknownModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { 74 | if m.setState(req.State, req.Plan, resp.PlanValue) { 75 | resp.PlanValue = req.StateValue 76 | } 77 | } 78 | 79 | //func (m AlwaysUseStateForUnknownModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { 80 | // if req.AttributeState == nil || resp.AttributePlan == nil || req.AttributeConfig == nil { 81 | // return 82 | // } 83 | // 84 | // // if we're creating the resource, no need to modify 85 | // if req.State.Raw.IsNull() { 86 | // return 87 | // } 88 | // 89 | // // if we're deleting the resource, no need to modify 90 | // if req.Plan.Raw.IsNull() { 91 | // return 92 | // } 93 | // 94 | // // if it's not planned to be the unknown value, stick with the concrete plan 95 | // if !resp.AttributePlan.IsUnknown() { 96 | // return 97 | // } 98 | // 99 | // resp.AttributePlan = req.AttributeState 100 | //} 101 | 102 | func (m AlwaysUseStateForUnknownModifier) setState(reqState tfsdk.State, reqPlan tfsdk.Plan, respAttrPlan attr.Value) bool { 103 | // if we're creating the resource, no need to modify 104 | if reqState.Raw.IsNull() { 105 | return false 106 | } 107 | 108 | // if we're deleting the resource, no need to modify 109 | if reqPlan.Raw.IsNull() { 110 | return false 111 | } 112 | 113 | // if it's not planned to be the unknown value, stick with the concrete plan 114 | return respAttrPlan.IsUnknown() 115 | } 116 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/containers/podman/v4/pkg/bindings" 9 | "github.com/hashicorp/terraform-plugin-framework/datasource" 10 | "github.com/hashicorp/terraform-plugin-framework/diag" 11 | "github.com/hashicorp/terraform-plugin-framework/provider" 12 | "github.com/hashicorp/terraform-plugin-framework/provider/schema" 13 | "github.com/hashicorp/terraform-plugin-framework/resource" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | "github.com/hashicorp/terraform-plugin-log/tflog" 16 | ) 17 | 18 | const ( 19 | podmanDefaultURI = "unix:///run/podman/podman.sock" 20 | ) 21 | 22 | // podmanProvider satisfies the provider.Provider interface and usually is included 23 | // with all Resource and DataSource implementations. 24 | type podmanProvider struct{} 25 | 26 | // providerData can be used to store data from the Terraform configuration. 27 | type providerData struct { 28 | URI types.String `tfsdk:"uri"` 29 | Identity types.String `tfsdk:"identity"` 30 | } 31 | 32 | // New creates a new podman provider. 33 | func New() provider.Provider { 34 | return &podmanProvider{} 35 | } 36 | 37 | // Metadata returns the provider type name. 38 | func (p *podmanProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { 39 | resp.TypeName = "podman" 40 | } 41 | 42 | // Schema defines the provider-level schema for configuration data. 43 | func (p *podmanProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { 44 | resp.Schema = schema.Schema{ 45 | MarkdownDescription: "The Podman provider provides resource management via the remote API.", 46 | Attributes: map[string]schema.Attribute{ 47 | "uri": schema.StringAttribute{ 48 | Description: "Connection URI to the podman service.", 49 | MarkdownDescription: "Connection URI to the podman service. " + 50 | "A valid URI connection should be of `scheme://`. " + 51 | "For example `tcp://localhost:`" + 52 | "or `unix:///run/podman/podman.sock`" + 53 | "or `ssh://@[:port]/run/podman/podman.sock?secure=True`." + 54 | "Defaults to `" + podmanDefaultURI + "`.", 55 | Optional: true, 56 | }, 57 | "identity": schema.StringAttribute{ 58 | Description: "Local path to the identity file for SSH based connections.", 59 | Optional: true, 60 | }, 61 | }, 62 | } 63 | } 64 | 65 | // Configure prepares a HashiCups API client for data sources and resources. 66 | func (p *podmanProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { 67 | tflog.Debug(ctx, "Configure Podman client") 68 | 69 | var data providerData 70 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 71 | if resp.Diagnostics.HasError() { 72 | return 73 | } 74 | 75 | newPodmanClient(ctx, &resp.Diagnostics, data) 76 | if resp.Diagnostics.HasError() { 77 | return 78 | } 79 | 80 | // make podman clent data available 81 | resp.DataSourceData = data 82 | resp.ResourceData = data 83 | 84 | tflog.Info(ctx, "Configured Podman client", map[string]any{"success": true}) 85 | } 86 | 87 | // DataSources defines the data sources implemented in the provider. 88 | func (p *podmanProvider) DataSources(ctx context.Context) []func() datasource.DataSource { 89 | return []func() datasource.DataSource{} 90 | } 91 | 92 | // Resources defines the resources implemented in the provider. 93 | func (p *podmanProvider) Resources(ctx context.Context) []func() resource.Resource { 94 | return []func() resource.Resource{ 95 | NewNetworkResource, 96 | NewPodResource, 97 | NewVolumeResource, 98 | } 99 | } 100 | 101 | // newPodmanClient initializes a new podman connection for further usage 102 | // The final client is the configured connection context 103 | func newPodmanClient(ctx context.Context, diags *diag.Diagnostics, data providerData) context.Context { 104 | // set default to local socket 105 | uri := podmanDefaultURI 106 | 107 | // only used for tests 108 | if testuri := os.Getenv("TF_ACC_TEST_PROVIDER_PODMAN_URI"); testuri != "" { 109 | uri = testuri 110 | } 111 | 112 | if data.URI.ValueString() != "" { 113 | uri = data.URI.ValueString() 114 | } 115 | 116 | c, err := bindings.NewConnectionWithIdentity(ctx, uri, data.Identity.ValueString(), false) 117 | if err != nil { 118 | diags.AddError("Failed to initialize connection to podman server", fmt.Sprintf("URI: %s, error: %s", uri, err.Error())) 119 | } 120 | 121 | return c 122 | } 123 | -------------------------------------------------------------------------------- /docs/resources/pod.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "podman_pod Resource - terraform-provider-podman" 4 | subcategory: "" 5 | description: |- 6 | Manage pods for containers 7 | --- 8 | 9 | # podman_pod (Resource) 10 | 11 | Manage pods for containers 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | # Create default local volume 17 | resource "podman_volume" "vol" {} 18 | 19 | # A pod with mounts 20 | resource "podman_pod" "pod" { 21 | name = "mypod" 22 | hostname = "superhost" 23 | mounts = [ 24 | { 25 | destination = "/mount" 26 | bind = { 27 | path = "/mnt/local" 28 | chown = true 29 | } 30 | }, 31 | { 32 | destination = "/data" 33 | volume = { 34 | name = podman_volume.vol.name 35 | read_only = true 36 | } 37 | }, 38 | ] 39 | } 40 | ``` 41 | 42 | 43 | ## Schema 44 | 45 | ### Optional 46 | 47 | - `cgroup_parent` (String) Path to cgroups under which the cgroup for the pod will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. 48 | - `hostname` (String) Hostname is the pod's hostname. If not set, the name of the pod will be used (if a name was not provided here, the name auto-generated for the pod will be used). This will be used by the infra container and all containers in the pod as long as the UTS namespace is shared. 49 | - `labels` (Map of String) Labels is a set of user defined key-value labels of the resource 50 | - `mounts` (Attributes Set) Mounts volume, bind, image, tmpfs, etc.. (see [below for nested schema](#nestedatt--mounts)) 51 | - `name` (String) Name of the resource, also used as ID. If not given a name will be automatically assigned. 52 | 53 | ### Read-Only 54 | 55 | - `id` (String) ID of the resource 56 | 57 | 58 | ### Nested Schema for `mounts` 59 | 60 | Required: 61 | 62 | - `destination` (String) Target path 63 | 64 | Optional: 65 | 66 | - `bind` (Attributes) Bind Volume (see [below for nested schema](#nestedatt--mounts--bind)) 67 | - `volume` (Attributes) Named Volume (see [below for nested schema](#nestedatt--mounts--volume)) 68 | 69 | 70 | ### Nested Schema for `mounts.bind` 71 | 72 | Required: 73 | 74 | - `path` (String) Host path 75 | 76 | Optional: 77 | 78 | - `chown` (Boolean) Change recursively the owner and group of the source volume based on the UID and GID of the container. 79 | - `dev` (Boolean) Mounting the volume with the nodev(false) option means that no devices on the volume will be able to be used by processes within the container.By default volumes are mounted with nodev. 80 | - `exec` (Boolean) Mounting the volume with the noexec(false) option means that no executables on the volume will be able to executed within the pod.Defaults depends on the mount type or storage driver. 81 | - `idmap` (Boolean) If specified, create an idmapped mount to the target user namespace in the container. 82 | - `propagation` (String) One of shared,slave,private,unbindable,rshared,rslave,rprivate,runbindable. 83 | - `read_only` (Boolean) Mount as read only. Default depends on the mount type. 84 | - `recursive` (Boolean) Set up a recursive bind mount. By default it is recursive. 85 | - `relabel` (Boolean) Labels the volume mounts. Sets the z (true) flag label the content with a shared content label, or Z (false) flag to label the content with a private unshared label. Default is unset (null). 86 | - `suid` (Boolean) Mounting the volume with the nosuid(false) options means that SUID applications on the volume will not be able to change their privilege.By default volumes are mounted with nosuid. 87 | 88 | 89 | 90 | ### Nested Schema for `mounts.volume` 91 | 92 | Required: 93 | 94 | - `name` (String) Name of the volume 95 | 96 | Optional: 97 | 98 | - `chown` (Boolean) Change recursively the owner and group of the source volume based on the UID and GID of the container. 99 | - `dev` (Boolean) Mounting the volume with the nodev(false) option means that no devices on the volume will be able to be used by processes within the container.By default volumes are mounted with nodev. 100 | - `exec` (Boolean) Mounting the volume with the noexec(false) option means that no executables on the volume will be able to executed within the pod.Defaults depends on the mount type or storage driver. 101 | - `idmap` (Boolean) If specified, create an idmapped mount to the target user namespace in the container. 102 | - `read_only` (Boolean) Mount as read only. Default depends on the mount type. 103 | - `suid` (Boolean) Mounting the volume with the nosuid(false) options means that SUID applications on the volume will not be able to change their privilege.By default volumes are mounted with nosuid. 104 | 105 | 106 | -------------------------------------------------------------------------------- /internal/modifier/replace_computed.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/attr" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/tfsdk" 9 | ) 10 | 11 | // RequiresReplaceComputed is a modified version of RequiresPlace, it allows also the replacement when computed values do change! 12 | func RequiresReplaceComputed() requiresReplaceModifierComputed { 13 | return requiresReplaceModifierComputed{} 14 | } 15 | 16 | // requiresReplaceModifierComputed is an AttributePlanModifier that sets RequiresReplace on the attribute. 17 | type requiresReplaceModifierComputed struct{} 18 | 19 | func (r requiresReplaceModifierComputed) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { 20 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 21 | } 22 | 23 | func (r requiresReplaceModifierComputed) PlanModifyFloat64(_ context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { 24 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 25 | } 26 | 27 | func (r requiresReplaceModifierComputed) PlanModifyInt64(_ context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { 28 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 29 | } 30 | 31 | func (r requiresReplaceModifierComputed) PlanModifyList(_ context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { 32 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 33 | } 34 | 35 | func (r requiresReplaceModifierComputed) PlanModifyMap(_ context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { 36 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 37 | } 38 | 39 | func (r requiresReplaceModifierComputed) PlanModifyNumber(_ context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { 40 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 41 | } 42 | 43 | func (r requiresReplaceModifierComputed) PlanModifyObject(_ context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { 44 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 45 | } 46 | 47 | func (r requiresReplaceModifierComputed) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { 48 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 49 | } 50 | 51 | func (r requiresReplaceModifierComputed) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { 52 | resp.RequiresReplace = r.replace(req.State, req.Plan, req.StateValue, req.PlanValue) 53 | } 54 | 55 | // Modify fills the AttributePlanModifier interface. 56 | // func (r requiresReplaceModifierComputed) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { 57 | // if req.AttributeConfig == nil || req.AttributePlan == nil || req.AttributeState == nil { 58 | // // shouldn't happen, but let's not panic if it does 59 | // return 60 | // } 61 | // 62 | // if req.State.Raw.IsNull() { 63 | // // if we're creating the resource, no need to delete and 64 | // // recreate it 65 | // return 66 | // } 67 | // 68 | // if req.Plan.Raw.IsNull() { 69 | // // if we're deleting the resource, no need to delete and 70 | // // recreate it 71 | // return 72 | // } 73 | // 74 | // if req.AttributePlan.Equal(req.AttributeState) { 75 | // // if the plan and the state are in agreement, this attribute 76 | // // isn't changing, don't require replace 77 | // return 78 | // } 79 | // 80 | // resp.RequiresReplace = true 81 | // } 82 | 83 | func (r requiresReplaceModifierComputed) replace(reqState tfsdk.State, reqPlan tfsdk.Plan, reqAttrState attr.Value, reqAttrPlan attr.Value) bool { 84 | if reqState.Raw.IsNull() { 85 | // if we're creating the resource, no need to delete and 86 | // recreate it 87 | return false 88 | } 89 | 90 | if reqPlan.Raw.IsNull() { 91 | // if we're deleting the resource, no need to delete and 92 | // recreate it 93 | return false 94 | } 95 | 96 | // if the plan and the state are in agreement, this attribute 97 | // isn't changing, don't require replace 98 | return !reqAttrPlan.Equal(reqAttrState) 99 | } 100 | 101 | // Description returns a human-readable description of the plan modifier. 102 | func (r requiresReplaceModifierComputed) Description(ctx context.Context) string { 103 | return "If the value of this attribute changes, Terraform will destroy and recreate the resource." 104 | } 105 | 106 | // MarkdownDescription returns a markdown description of the plan modifier. 107 | func (r requiresReplaceModifierComputed) MarkdownDescription(ctx context.Context) string { 108 | return "If the value of this attribute changes, Terraform will destroy and recreate the resource." 109 | } 110 | -------------------------------------------------------------------------------- /internal/provider/resource_pod.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/containers/podman/v4/pkg/domain/entities" 8 | "github.com/containers/podman/v4/pkg/specgen" 9 | "github.com/hashicorp/terraform-plugin-framework/diag" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | "github.com/project0/terraform-provider-podman/internal/modifier" 16 | "github.com/project0/terraform-provider-podman/internal/provider/shared" 17 | "github.com/project0/terraform-provider-podman/internal/utils" 18 | ) 19 | 20 | type ( 21 | podResource struct { 22 | genericResource 23 | } 24 | podResourceData struct { 25 | ID types.String `tfsdk:"id"` 26 | Name types.String `tfsdk:"name"` 27 | Labels types.Map `tfsdk:"labels"` 28 | 29 | CgroupParent types.String `tfsdk:"cgroup_parent"` 30 | Hostname types.String `tfsdk:"hostname"` 31 | 32 | Mounts shared.Mounts `tfsdk:"mounts"` 33 | } 34 | ) 35 | 36 | // Ensure the implementation satisfies the expected interfaces. 37 | var ( 38 | _ resource.Resource = &podResource{} 39 | _ resource.ResourceWithConfigure = &podResource{} 40 | _ resource.ResourceWithImportState = &podResource{} 41 | ) 42 | 43 | // NewPodResource creates a new pod resource. 44 | func NewPodResource() resource.Resource { 45 | return &podResource{} 46 | } 47 | 48 | // Configure adds the provider configured client to the resource. 49 | func (r *podResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 50 | r.genericResource.Configure(ctx, req, resp) 51 | } 52 | 53 | // Metadata returns the resource type name. 54 | func (r podResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 55 | resp.TypeName = req.ProviderTypeName + "_pod" 56 | } 57 | 58 | // Schema returns the resource schema. 59 | func (r podResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 60 | mountsAttr := make(shared.Mounts, 0) 61 | resp.Schema = schema.Schema{ 62 | Description: "Manage pods for containers", 63 | Attributes: withGenericAttributes( 64 | map[string]schema.Attribute{ 65 | "cgroup_parent": schema.StringAttribute{ 66 | MarkdownDescription: "Path to cgroups under which the cgroup for the pod will be created. " + 67 | "If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. " + 68 | "Cgroups will be created if they do not already exist.", 69 | Required: false, 70 | Optional: true, 71 | Computed: true, 72 | PlanModifiers: []planmodifier.String{ 73 | stringplanmodifier.UseStateForUnknown(), 74 | modifier.RequiresReplaceComputed(), 75 | }, 76 | }, 77 | "hostname": schema.StringAttribute{ 78 | Description: "Hostname is the pod's hostname. " + 79 | "If not set, the name of the pod will be used (if a name was not provided here, the name auto-generated for the pod will be used). " + 80 | "This will be used by the infra container and all containers in the pod as long as the UTS namespace is shared.", 81 | Required: false, 82 | Optional: true, 83 | Computed: false, 84 | PlanModifiers: []planmodifier.String{ 85 | stringplanmodifier.UseStateForUnknown(), 86 | stringplanmodifier.RequiresReplace(), 87 | }, 88 | }, 89 | "mounts": mountsAttr.GetSchema(ctx), 90 | }, 91 | ), 92 | } 93 | } 94 | 95 | func toPodmanPodSpecGenerator(ctx context.Context, d podResourceData, diags *diag.Diagnostics) *specgen.PodSpecGenerator { 96 | s := specgen.NewPodSpecGenerator() 97 | p := &entities.PodCreateOptions{ 98 | Name: d.Name.ValueString(), 99 | CgroupParent: d.CgroupParent.ValueString(), 100 | Hostname: d.Hostname.ValueString(), 101 | Infra: true, 102 | } 103 | 104 | diags.Append(d.Labels.ElementsAs(ctx, &p.Labels, true)...) 105 | sp, err := entities.ToPodSpecGen(*s, p) 106 | if err != nil { 107 | diags.AddError("Invalid pod configuration", fmt.Sprintf("Cannot build pod configuration: %q", err.Error())) 108 | } 109 | // add storage 110 | sp.Volumes, sp.Mounts = d.Mounts.ToPodmanSpec(diags) 111 | if err := sp.Validate(); err != nil { 112 | diags.AddError("Invalid pod configuration", fmt.Sprintf("Cannot build pod configuration: %q", err.Error())) 113 | } 114 | return sp 115 | } 116 | 117 | func fromPodResponse(p *entities.PodInspectReport, diags *diag.Diagnostics) *podResourceData { 118 | hostname := types.StringNull() 119 | if p.Hostname != "" { 120 | hostname = types.StringValue(p.Hostname) 121 | } 122 | 123 | d := &podResourceData{ 124 | ID: types.StringValue(p.ID), 125 | Name: types.StringValue(p.Name), 126 | Labels: utils.MapStringToMapType(p.Labels, diags), 127 | Mounts: shared.FromPodmanToMounts(diags, p.Mounts), 128 | CgroupParent: types.StringValue(p.CgroupParent), 129 | Hostname: hostname, 130 | } 131 | 132 | return d 133 | } 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/project0/terraform-provider-podman 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/containers/common v0.51.0 7 | github.com/containers/podman/v4 v4.4.0 8 | github.com/hashicorp/terraform-plugin-docs v0.13.0 9 | github.com/hashicorp/terraform-plugin-framework v1.1.1 10 | github.com/hashicorp/terraform-plugin-framework-validators v0.9.0 11 | github.com/hashicorp/terraform-plugin-go v0.14.3 12 | github.com/hashicorp/terraform-plugin-log v0.7.0 13 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 14 | github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 15 | ) 16 | 17 | require ( 18 | github.com/BurntSushi/toml v1.2.1 // indirect 19 | github.com/Masterminds/goutils v1.1.1 // indirect 20 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 21 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect 22 | github.com/Microsoft/go-winio v0.6.0 // indirect 23 | github.com/Microsoft/hcsshim v0.9.6 // indirect 24 | github.com/VividCortex/ewma v1.2.0 // indirect 25 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 26 | github.com/agext/levenshtein v1.2.2 // indirect 27 | github.com/apparentlymart/go-cidr v1.1.0 // indirect 28 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 29 | github.com/armon/go-radix v1.0.0 // indirect 30 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 31 | github.com/bgentry/speakeasy v0.1.0 // indirect 32 | github.com/blang/semver/v4 v4.0.0 // indirect 33 | github.com/chzyer/readline v1.5.1 // indirect 34 | github.com/cilium/ebpf v0.9.3 // indirect 35 | github.com/container-orchestrated-devices/container-device-interface v0.5.3 // indirect 36 | github.com/containerd/cgroups v1.0.4 // indirect 37 | github.com/containerd/containerd v1.6.15 // indirect 38 | github.com/containerd/stargz-snapshotter/estargz v0.13.0 // indirect 39 | github.com/containers/buildah v1.29.0 // indirect 40 | github.com/containers/image/v5 v5.24.0 // indirect 41 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 42 | github.com/containers/ocicrypt v1.1.7 // indirect 43 | github.com/containers/psgo v1.8.0 // indirect 44 | github.com/containers/storage v1.45.3 // indirect 45 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 46 | github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect 47 | github.com/cyphar/filepath-securejoin v0.2.3 // indirect 48 | github.com/davecgh/go-spew v1.1.1 // indirect 49 | github.com/disiqueira/gotree/v3 v3.0.2 // indirect 50 | github.com/docker/distribution v2.8.1+incompatible // indirect 51 | github.com/docker/docker v20.10.23+incompatible // indirect 52 | github.com/docker/docker-credential-helpers v0.7.0 // indirect 53 | github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 // indirect 54 | github.com/docker/go-units v0.5.0 // indirect 55 | github.com/fatih/color v1.13.0 // indirect 56 | github.com/fsnotify/fsnotify v1.6.0 // indirect 57 | github.com/ghodss/yaml v1.0.0 // indirect 58 | github.com/go-openapi/analysis v0.21.4 // indirect 59 | github.com/go-openapi/errors v0.20.3 // indirect 60 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 61 | github.com/go-openapi/jsonreference v0.20.0 // indirect 62 | github.com/go-openapi/loads v0.21.2 // indirect 63 | github.com/go-openapi/runtime v0.24.1 // indirect 64 | github.com/go-openapi/spec v0.20.7 // indirect 65 | github.com/go-openapi/strfmt v0.21.3 // indirect 66 | github.com/go-openapi/swag v0.22.3 // indirect 67 | github.com/go-openapi/validate v0.22.0 // indirect 68 | github.com/godbus/dbus/v5 v5.1.1-0.20221029134443-4b691ce883d5 // indirect 69 | github.com/gogo/protobuf v1.3.2 // indirect 70 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 71 | github.com/golang/protobuf v1.5.2 // indirect 72 | github.com/google/go-cmp v0.5.9 // indirect 73 | github.com/google/go-containerregistry v0.12.1 // indirect 74 | github.com/google/go-intervals v0.0.2 // indirect 75 | github.com/google/uuid v1.3.0 // indirect 76 | github.com/gorilla/mux v1.8.0 // indirect 77 | github.com/gorilla/schema v1.2.0 // indirect 78 | github.com/hashicorp/errwrap v1.1.0 // indirect 79 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 80 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 81 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect 82 | github.com/hashicorp/go-hclog v1.3.1 // indirect 83 | github.com/hashicorp/go-multierror v1.1.1 // indirect 84 | github.com/hashicorp/go-plugin v1.4.8 // indirect 85 | github.com/hashicorp/go-uuid v1.0.3 // indirect 86 | github.com/hashicorp/go-version v1.6.0 // indirect 87 | github.com/hashicorp/hc-install v0.4.0 // indirect 88 | github.com/hashicorp/hcl/v2 v2.15.0 // indirect 89 | github.com/hashicorp/logutils v1.0.0 // indirect 90 | github.com/hashicorp/terraform-exec v0.17.3 // indirect 91 | github.com/hashicorp/terraform-json v0.14.0 // indirect 92 | github.com/hashicorp/terraform-registry-address v0.1.0 // indirect 93 | github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect 94 | github.com/hashicorp/yamux v0.1.1 // indirect 95 | github.com/huandu/xstrings v1.3.2 // indirect 96 | github.com/imdario/mergo v0.3.13 // indirect 97 | github.com/jinzhu/copier v0.3.5 // indirect 98 | github.com/josharian/intern v1.0.0 // indirect 99 | github.com/json-iterator/go v1.1.12 // indirect 100 | github.com/klauspost/compress v1.15.15 // indirect 101 | github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 // indirect 102 | github.com/kr/fs v0.1.0 // indirect 103 | github.com/letsencrypt/boulder v0.0.0-20221130210056-b7e4e9d0ce62 // indirect 104 | github.com/mailru/easyjson v0.7.7 // indirect 105 | github.com/manifoldco/promptui v0.9.0 // indirect 106 | github.com/mattn/go-colorable v0.1.13 // indirect 107 | github.com/mattn/go-isatty v0.0.16 // indirect 108 | github.com/mattn/go-runewidth v0.0.14 // indirect 109 | github.com/mattn/go-shellwords v1.0.12 // indirect 110 | github.com/miekg/pkcs11 v1.1.1 // indirect 111 | github.com/mistifyio/go-zfs/v3 v3.0.0 // indirect 112 | github.com/mitchellh/cli v1.1.4 // indirect 113 | github.com/mitchellh/copystructure v1.2.0 // indirect 114 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 115 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 116 | github.com/mitchellh/mapstructure v1.5.0 // indirect 117 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 118 | github.com/moby/sys/mountinfo v0.6.2 // indirect 119 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 120 | github.com/modern-go/reflect2 v1.0.2 // indirect 121 | github.com/nxadm/tail v1.4.8 // indirect 122 | github.com/oklog/run v1.1.0 // indirect 123 | github.com/oklog/ulid v1.3.1 // indirect 124 | github.com/opencontainers/go-digest v1.0.0 // indirect 125 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 126 | github.com/opencontainers/runc v1.1.4 // indirect 127 | github.com/opencontainers/runtime-tools v0.9.1-0.20221107153022-2802ff9ff545 // indirect 128 | github.com/opencontainers/selinux v1.10.2 // indirect 129 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect 130 | github.com/pkg/errors v0.9.1 // indirect 131 | github.com/pkg/sftp v1.13.5 // indirect 132 | github.com/posener/complete v1.2.3 // indirect 133 | github.com/proglottis/gpgme v0.1.3 // indirect 134 | github.com/rivo/uniseg v0.4.3 // indirect 135 | github.com/russross/blackfriday v1.6.0 // indirect 136 | github.com/shopspring/decimal v1.3.1 // indirect 137 | github.com/sigstore/fulcio v1.0.0 // indirect 138 | github.com/sigstore/rekor v1.0.1 // indirect 139 | github.com/sigstore/sigstore v1.5.1 // indirect 140 | github.com/sirupsen/logrus v1.9.0 // indirect 141 | github.com/spf13/cast v1.5.0 // indirect 142 | github.com/spf13/pflag v1.0.5 // indirect 143 | github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect 144 | github.com/sylabs/sif/v2 v2.9.0 // indirect 145 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect 146 | github.com/tchap/go-patricia v2.3.0+incompatible // indirect 147 | github.com/theupdateframework/go-tuf v0.5.2-0.20221207161717-9cb61d6e65f5 // indirect 148 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 149 | github.com/ulikunitz/xz v0.5.11 // indirect 150 | github.com/vbatts/tar-split v0.11.2 // indirect 151 | github.com/vbauerster/mpb/v7 v7.5.3 // indirect 152 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 153 | github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect 154 | github.com/vmihailenco/tagparser v0.1.2 // indirect 155 | github.com/zclconf/go-cty v1.12.1 // indirect 156 | go.etcd.io/bbolt v1.3.6 // indirect 157 | go.mongodb.org/mongo-driver v1.11.1 // indirect 158 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect 159 | go.opencensus.io v0.24.0 // indirect 160 | golang.org/x/crypto v0.5.0 // indirect 161 | golang.org/x/mod v0.7.0 // indirect 162 | golang.org/x/net v0.5.0 // indirect 163 | golang.org/x/sync v0.1.0 // indirect 164 | golang.org/x/sys v0.4.0 // indirect 165 | golang.org/x/term v0.4.0 // indirect 166 | golang.org/x/text v0.6.0 // indirect 167 | golang.org/x/tools v0.4.0 // indirect 168 | google.golang.org/appengine v1.6.7 // indirect 169 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect 170 | google.golang.org/grpc v1.51.0 // indirect 171 | google.golang.org/protobuf v1.28.1 // indirect 172 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 173 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 174 | gopkg.in/yaml.v2 v2.4.0 // indirect 175 | gopkg.in/yaml.v3 v3.0.1 // indirect 176 | sigs.k8s.io/yaml v1.3.0 // indirect 177 | ) 178 | -------------------------------------------------------------------------------- /internal/provider/resource_network.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | ntypes "github.com/containers/common/libnetwork/types" 9 | 10 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | "github.com/hashicorp/terraform-plugin-framework/resource" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 16 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 17 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 18 | "github.com/hashicorp/terraform-plugin-framework/types" 19 | "github.com/project0/terraform-provider-podman/internal/modifier" 20 | "github.com/project0/terraform-provider-podman/internal/utils" 21 | "github.com/project0/terraform-provider-podman/internal/validators" 22 | ) 23 | 24 | type ( 25 | networkResource struct { 26 | genericResource 27 | } 28 | 29 | networkResourceData struct { 30 | ID types.String `tfsdk:"id"` 31 | Name types.String `tfsdk:"name"` 32 | Labels types.Map `tfsdk:"labels"` 33 | 34 | DNS types.Bool `tfsdk:"dns"` 35 | IPv6 types.Bool `tfsdk:"ipv6"` 36 | Internal types.Bool `tfsdk:"internal"` 37 | 38 | Driver types.String `tfsdk:"driver"` 39 | IPAMDriver types.String `tfsdk:"ipam_driver"` 40 | Options types.Map `tfsdk:"options"` 41 | 42 | Subnets []networkResourceSubnetData `tfsdk:"subnets"` 43 | } 44 | 45 | networkResourceSubnetData struct { 46 | Subnet types.String `tfsdk:"subnet"` 47 | Gateway types.String `tfsdk:"gateway"` 48 | } 49 | ) 50 | 51 | // Ensure the implementation satisfies the expected interfaces. 52 | var ( 53 | _ resource.Resource = &networkResource{} 54 | _ resource.ResourceWithConfigure = &networkResource{} 55 | _ resource.ResourceWithImportState = &networkResource{} 56 | ) 57 | 58 | // NewNetworkResource creates a new network resource. 59 | func NewNetworkResource() resource.Resource { 60 | return &networkResource{} 61 | } 62 | 63 | // Configure adds the provider configured client to the resource. 64 | func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 65 | r.genericResource.Configure(ctx, req, resp) 66 | } 67 | 68 | // Metadata returns the resource type name. 69 | func (r networkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 70 | resp.TypeName = req.ProviderTypeName + "_network" 71 | } 72 | 73 | // Schema returns the resource schema. 74 | func (r networkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 75 | resp.Schema = schema.Schema{ 76 | Description: "Manage networks for containers and pods", 77 | Attributes: withGenericAttributes( 78 | map[string]schema.Attribute{ 79 | "dns": schema.BoolAttribute{ 80 | MarkdownDescription: "Enable the DNS plugin for this network which if enabled, can perform container to container name resolution. Defaults to `false`.", 81 | Computed: true, 82 | Optional: true, 83 | PlanModifiers: []planmodifier.Bool{ 84 | boolplanmodifier.UseStateForUnknown(), 85 | modifier.RequiresReplaceComputed(), 86 | }, 87 | }, 88 | 89 | "ipv6": schema.BoolAttribute{ 90 | MarkdownDescription: "Enable IPv6 (Dual Stack) networking. If no subnets are given it will allocate a ipv4 and ipv6 subnet. Defaults to `false`.", 91 | Computed: true, 92 | Optional: true, 93 | PlanModifiers: []planmodifier.Bool{ 94 | boolplanmodifier.UseStateForUnknown(), 95 | modifier.RequiresReplaceComputed(), 96 | }, 97 | }, 98 | 99 | "internal": schema.BoolAttribute{ 100 | MarkdownDescription: "Internal is whether the Network should not have external routes to public or other Networks. Defaults to `false`.", 101 | Computed: true, 102 | Optional: true, 103 | PlanModifiers: []planmodifier.Bool{ 104 | boolplanmodifier.UseStateForUnknown(), 105 | modifier.RequiresReplaceComputed(), 106 | }, 107 | }, 108 | 109 | "driver": schema.StringAttribute{ 110 | MarkdownDescription: fmt.Sprintf( 111 | "Driver to manage the network. One of `%s`, `%s`, `%s` are currently supported. By podman defaults to `bridge`.", 112 | ntypes.BridgeNetworkDriver, 113 | ntypes.MacVLANNetworkDriver, 114 | ntypes.IPVLANNetworkDriver, 115 | ), 116 | Computed: true, 117 | Optional: true, 118 | Validators: []validator.String{ 119 | stringvalidator.OneOf( 120 | ntypes.BridgeNetworkDriver, 121 | ntypes.MacVLANNetworkDriver, 122 | ntypes.IPVLANNetworkDriver, 123 | ), 124 | }, 125 | PlanModifiers: []planmodifier.String{ 126 | stringplanmodifier.UseStateForUnknown(), 127 | modifier.RequiresReplaceComputed(), 128 | }, 129 | }, 130 | 131 | "ipam_driver": schema.StringAttribute{ 132 | Computed: true, 133 | Optional: true, 134 | MarkdownDescription: fmt.Sprintf( 135 | "Set the ipam driver (IP Address Management Driver) for the network. Valid values are `%s`, `%s`, `%s`. When unset podman will choose an ipam driver automatically based on the network driver.", 136 | ntypes.HostLocalIPAMDriver, 137 | ntypes.DHCPIPAMDriver, 138 | "none", 139 | ), 140 | Validators: []validator.String{ 141 | stringvalidator.OneOf( 142 | ntypes.HostLocalIPAMDriver, 143 | ntypes.DHCPIPAMDriver, 144 | "none", 145 | ), 146 | }, 147 | PlanModifiers: []planmodifier.String{ 148 | stringplanmodifier.UseStateForUnknown(), 149 | modifier.RequiresReplaceComputed(), 150 | }, 151 | }, 152 | 153 | "options": schema.MapAttribute{ 154 | Description: "Driver specific options.", 155 | Required: false, 156 | Optional: true, 157 | Computed: true, 158 | ElementType: types.StringType, 159 | PlanModifiers: []planmodifier.Map{ 160 | modifier.UseDefaultModifier(utils.MapStringEmpty()), 161 | modifier.RequiresReplaceComputed(), 162 | }, 163 | }, 164 | 165 | "subnets": schema.SetNestedAttribute{ 166 | Description: "Subnets for this network.", 167 | Required: false, 168 | Optional: true, 169 | Computed: true, 170 | NestedObject: schema.NestedAttributeObject{ 171 | Attributes: map[string]schema.Attribute{ 172 | "subnet": schema.StringAttribute{ 173 | MarkdownDescription: "The subnet in CIDR notation.", 174 | Required: true, 175 | Optional: false, 176 | Validators: []validator.String{ 177 | validators.IsCIDR(), 178 | }, 179 | PlanModifiers: []planmodifier.String{ 180 | stringplanmodifier.RequiresReplace(), 181 | }, 182 | }, 183 | "gateway": schema.StringAttribute{ 184 | MarkdownDescription: "Gateway IP for this Network.", 185 | Computed: true, 186 | Optional: true, 187 | Validators: []validator.String{ 188 | validators.IsIpAdress(), 189 | }, 190 | PlanModifiers: []planmodifier.String{ 191 | stringplanmodifier.UseStateForUnknown(), 192 | modifier.RequiresReplaceComputed(), 193 | }, 194 | }, 195 | }, 196 | }, 197 | }, 198 | }, 199 | ), 200 | } 201 | } 202 | 203 | // toPodmanNetwork converts a resource data to a podman network 204 | func toPodmanNetwork(ctx context.Context, d networkResourceData, diags *diag.Diagnostics) *ntypes.Network { 205 | var nw = &ntypes.Network{ 206 | Name: d.Name.ValueString(), 207 | Driver: d.Driver.ValueString(), 208 | IPv6Enabled: d.IPv6.ValueBool(), 209 | DNSEnabled: d.DNS.ValueBool(), 210 | Internal: d.Internal.ValueBool(), 211 | } 212 | 213 | // Convert map types 214 | diags.Append(d.Labels.ElementsAs(ctx, &nw.Labels, true)...) 215 | diags.Append(d.Options.ElementsAs(ctx, &nw.Options, true)...) 216 | 217 | if !d.IPAMDriver.IsNull() { 218 | ipam := map[string]string{ 219 | "driver": d.IPAMDriver.ValueString(), 220 | } 221 | nw.IPAMOptions = ipam 222 | } 223 | 224 | // subnet 225 | for _, s := range d.Subnets { 226 | _, ipNet, err := net.ParseCIDR(s.Subnet.ValueString()) 227 | if err != nil { 228 | diags.AddError("Cannot parse subnet CIDR", err.Error()) 229 | continue 230 | } 231 | subnet := &ntypes.Subnet{ 232 | Subnet: ntypes.IPNet{ 233 | IPNet: *ipNet, 234 | }, 235 | } 236 | if !s.Gateway.IsNull() { 237 | subnet.Gateway = net.ParseIP(s.Gateway.ValueString()) 238 | } 239 | nw.Subnets = append(nw.Subnets, *subnet) 240 | } 241 | 242 | return nw 243 | } 244 | 245 | // fromNetwork converts a podman network to a resource data 246 | func fromPodmanNetwork(n ntypes.Network, diags *diag.Diagnostics) *networkResourceData { 247 | d := &networkResourceData{ 248 | ID: types.StringValue(n.Name), 249 | Name: types.StringValue(n.Name), 250 | DNS: types.BoolValue(n.DNSEnabled), 251 | IPv6: types.BoolValue(n.IPv6Enabled), 252 | Internal: types.BoolValue(n.Internal), 253 | Driver: types.StringValue(n.Driver), 254 | Labels: utils.MapStringToMapType(n.Labels, diags), 255 | Options: utils.MapStringToMapType(n.Options, diags), 256 | } 257 | 258 | d.IPAMDriver = utils.MapStringValueToStringType(n.IPAMOptions, "driver") 259 | 260 | for _, s := range n.Subnets { 261 | subnet := networkResourceSubnetData{ 262 | Subnet: types.StringValue(s.Subnet.String()), 263 | Gateway: types.StringValue(s.Gateway.String()), 264 | } 265 | d.Subnets = append(d.Subnets, subnet) 266 | } 267 | return d 268 | } 269 | -------------------------------------------------------------------------------- /internal/provider/shared/mount_options.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" 8 | "github.com/hashicorp/terraform-plugin-framework/diag" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 11 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 12 | "github.com/hashicorp/terraform-plugin-framework/types" 13 | "github.com/project0/terraform-provider-podman/internal/modifier" 14 | ) 15 | 16 | const ( 17 | bindPropagationShared = "shared" 18 | bindPropagationSlave = "slave" 19 | bindPropagationPrivate = "private" 20 | bindPropagationUnbindable = "unbindable" 21 | 22 | bindPropagationSharedRecursive = "rshared" 23 | bindPropagationSlaveRecursive = "rslave" 24 | bindPropagationPrivateRecursive = "rprivate" 25 | bindPropagationUnbindableRecursive = "runbindable" 26 | ) 27 | 28 | var ( 29 | bindPropagations = []string{ 30 | bindPropagationShared, 31 | bindPropagationSlave, 32 | bindPropagationPrivate, 33 | bindPropagationUnbindable, 34 | bindPropagationSharedRecursive, 35 | bindPropagationSlaveRecursive, 36 | bindPropagationPrivateRecursive, 37 | bindPropagationUnbindableRecursive, 38 | } 39 | ) 40 | 41 | func (m Mounts) attributeSchemaReadOnly() schema.Attribute { 42 | return schema.BoolAttribute{ 43 | Description: "Mount as read only. Default depends on the mount type.", 44 | Computed: true, 45 | Optional: true, 46 | PlanModifiers: []planmodifier.Bool{ 47 | modifier.AlwaysUseStateForUnknown(), 48 | modifier.RequiresReplaceComputed(), 49 | }, 50 | } 51 | } 52 | 53 | func (m Mounts) attributeSchemaSuid() schema.Attribute { 54 | return schema.BoolAttribute{ 55 | Description: "Mounting the volume with the nosuid(false) options means that SUID applications on the volume will not be able to change their privilege." + 56 | "By default volumes are mounted with nosuid.", 57 | Computed: true, 58 | Optional: true, 59 | PlanModifiers: []planmodifier.Bool{ 60 | modifier.AlwaysUseStateForUnknown(), 61 | modifier.RequiresReplaceComputed(), 62 | }, 63 | } 64 | } 65 | 66 | func (m Mounts) attributeSchemaExec() schema.Attribute { 67 | return schema.BoolAttribute{ 68 | Description: "Mounting the volume with the noexec(false) option means that no executables on the volume will be able to executed within the pod." + 69 | "Defaults depends on the mount type or storage driver.", 70 | Computed: true, 71 | Optional: true, 72 | PlanModifiers: []planmodifier.Bool{ 73 | modifier.AlwaysUseStateForUnknown(), 74 | modifier.RequiresReplaceComputed(), 75 | }, 76 | } 77 | } 78 | 79 | func (m Mounts) attributeSchemaDev() schema.Attribute { 80 | return schema.BoolAttribute{ 81 | Description: "Mounting the volume with the nodev(false) option means that no devices on the volume will be able to be used by processes within the container." + 82 | "By default volumes are mounted with nodev.", 83 | Computed: true, 84 | Optional: true, 85 | PlanModifiers: []planmodifier.Bool{ 86 | modifier.AlwaysUseStateForUnknown(), 87 | modifier.RequiresReplaceComputed(), 88 | }, 89 | } 90 | } 91 | 92 | func (m Mounts) attributeSchemaChown() schema.Attribute { 93 | return schema.BoolAttribute{ 94 | Description: "Change recursively the owner and group of the source volume based on the UID and GID of the container.", 95 | Computed: true, 96 | Optional: true, 97 | PlanModifiers: []planmodifier.Bool{ 98 | modifier.AlwaysUseStateForUnknown(), 99 | modifier.RequiresReplaceComputed(), 100 | }, 101 | } 102 | } 103 | 104 | func (m Mounts) attributeSchemaIDmap() schema.Attribute { 105 | return schema.BoolAttribute{ 106 | Description: "If specified, create an idmapped mount to the target user namespace in the container.", 107 | Computed: true, 108 | Optional: true, 109 | PlanModifiers: []planmodifier.Bool{ 110 | modifier.AlwaysUseStateForUnknown(), 111 | modifier.RequiresReplaceComputed(), 112 | }, 113 | } 114 | } 115 | 116 | func (m Mounts) attributeSchemaBindPropagation() schema.Attribute { 117 | return schema.StringAttribute{ 118 | Description: fmt.Sprintf("One of %s.", strings.Join(bindPropagations, ",")), 119 | Computed: true, 120 | Optional: true, 121 | Validators: []validator.String{ 122 | stringvalidator.OneOf(bindPropagations...), 123 | }, 124 | PlanModifiers: []planmodifier.String{ 125 | modifier.AlwaysUseStateForUnknown(), 126 | modifier.RequiresReplaceComputed(), 127 | }, 128 | } 129 | } 130 | 131 | func (m Mounts) attributeSchemaBindRecursive() schema.Attribute { 132 | return schema.BoolAttribute{ 133 | Description: "Set up a recursive bind mount. By default it is recursive.", 134 | Computed: true, 135 | Optional: true, 136 | PlanModifiers: []planmodifier.Bool{ 137 | modifier.AlwaysUseStateForUnknown(), 138 | modifier.RequiresReplaceComputed(), 139 | }, 140 | } 141 | } 142 | 143 | func (m Mounts) attributeSchemaBindRelabel() schema.Attribute { 144 | return schema.BoolAttribute{ 145 | Description: "Labels the volume mounts. Sets the z (true) flag label the content with a shared content label, " + 146 | "or Z (false) flag to label the content with a private unshared label. " + 147 | "Default is unset (null).", 148 | Computed: true, 149 | Optional: true, 150 | PlanModifiers: []planmodifier.Bool{ 151 | modifier.AlwaysUseStateForUnknown(), 152 | modifier.RequiresReplaceComputed(), 153 | }, 154 | } 155 | } 156 | 157 | // func (m Mounts) attributeSchemaTmpfsSize() schema.Attribute{ 158 | // return tfsdk.Attribute{ 159 | // Description: "Size of the tmpfs mount in bytes or units. Unlimited by default in Linux.", 160 | // Computed: true, 161 | // Optional: true, 162 | // Type: types.StringType, 163 | // Validators: []tfsdk.AttributeValidator{ 164 | // validator.MatchTmpfSize(), 165 | // }, 166 | // PlanModifiers: tfsdk.AttributePlanModifiers{ 167 | // modifier.AlwaysUseStateForUnknown(), 168 | // modifier.RequiresReplaceComputed(), 169 | // }, 170 | // } 171 | // } 172 | // 173 | // func (m Mounts) attributeSchemaTmpfsMode() schema.Attribute{ 174 | // return tfsdk.Attribute{ 175 | // Description: "File mode of the tmpfs in octal (e.g. 700 or 0700). Defaults to 1777 in Linux.", 176 | // Computed: true, 177 | // Optional: true, 178 | // Type: types.StringType, 179 | // Validators: []tfsdk.AttributeValidator{ 180 | // validator.MatchOctal(), 181 | // }, 182 | // PlanModifiers: tfsdk.AttributePlanModifiers{ 183 | // modifier.AlwaysUseStateForUnknown(), 184 | // modifier.RequiresReplaceComputed(), 185 | // }, 186 | // } 187 | // } 188 | // 189 | // func (m Mounts) attributeSchemaTmpfsTmpCopyUp() schema.Attribute{ 190 | // return tfsdk.Attribute{ 191 | // Description: "Enable copyup from the image directory at the same location to the tmpfs. Used by default.", 192 | // Computed: true, 193 | // Optional: true, 194 | // Type: types.BoolType, 195 | // PlanModifiers: tfsdk.AttributePlanModifiers{ 196 | // modifier.AlwaysUseStateForUnknown(), 197 | // modifier.RequiresReplaceComputed(), 198 | // }, 199 | // } 200 | // } 201 | 202 | type allMountOptions struct { 203 | readOnly types.Bool 204 | dev types.Bool 205 | exec types.Bool 206 | suid types.Bool 207 | chown types.Bool 208 | idmap types.Bool 209 | // bind 210 | recursive types.Bool 211 | relabel types.Bool 212 | propagation types.String 213 | // tmpfs 214 | size types.String 215 | mode types.String 216 | tmpcopyup types.Bool 217 | } 218 | 219 | func parseMountOptions(diags *diag.Diagnostics, options []string) allMountOptions { 220 | result := allMountOptions{ 221 | readOnly: types.BoolNull(), 222 | dev: types.BoolNull(), 223 | exec: types.BoolNull(), 224 | suid: types.BoolNull(), 225 | 226 | // chown and idmap is only present when flag is set, 227 | // consider it false when not present (default) 228 | chown: types.BoolValue(false), 229 | idmap: types.BoolValue(false), 230 | 231 | // bind 232 | recursive: types.BoolNull(), 233 | relabel: types.BoolNull(), 234 | propagation: types.StringNull(), 235 | 236 | // tmpfs 237 | size: types.StringNull(), 238 | mode: types.StringNull(), 239 | tmpcopyup: types.BoolNull(), 240 | } 241 | 242 | for _, o := range options { 243 | 244 | switch o { 245 | case "ro", "rw": 246 | result.readOnly = types.BoolValue((o == "ro")) 247 | 248 | case "dev", "nodev": 249 | result.dev = types.BoolValue((o == "dev")) 250 | 251 | case "exec", "noexec": 252 | result.exec = types.BoolValue((o == "exec")) 253 | 254 | case "suid", "nosuid": 255 | result.suid = types.BoolValue((o == "suid")) 256 | 257 | case "bind", "rbind": 258 | result.recursive = types.BoolValue((o == "rbind")) 259 | 260 | case "tmpcopyup", "notmpcopyup": 261 | result.recursive = types.BoolValue((o == "tmpcopyup")) 262 | 263 | // public = z (relabel), private = Z (no relabel) 264 | case "z", "Z": 265 | result.relabel = types.BoolValue((o == "z")) 266 | 267 | case "U": 268 | result.chown = types.BoolValue(true) 269 | 270 | case "idmap": 271 | result.idmap = types.BoolValue(true) 272 | 273 | case "size": 274 | result.size = types.StringValue(o) 275 | 276 | case "mode": 277 | result.mode = types.StringValue(o) 278 | 279 | case 280 | bindPropagationShared, 281 | bindPropagationSlave, 282 | bindPropagationPrivate, 283 | bindPropagationUnbindable, 284 | bindPropagationSharedRecursive, 285 | bindPropagationSlaveRecursive, 286 | bindPropagationPrivateRecursive, 287 | bindPropagationUnbindableRecursive: 288 | result.propagation = types.StringValue(o) 289 | 290 | default: 291 | diags.AddWarning("Unknown mount option retrieved", o) 292 | } 293 | } 294 | return result 295 | } 296 | -------------------------------------------------------------------------------- /internal/provider/shared/mount.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/containers/podman/v4/libpod/define" 7 | "github.com/containers/podman/v4/pkg/specgen" 8 | "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" 9 | "github.com/hashicorp/terraform-plugin-framework/diag" 10 | "github.com/hashicorp/terraform-plugin-framework/path" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 16 | "github.com/hashicorp/terraform-plugin-framework/types" 17 | "github.com/project0/terraform-provider-podman/internal/modifier" 18 | "github.com/project0/terraform-provider-podman/internal/validators" 19 | 20 | "github.com/opencontainers/runtime-spec/specs-go" 21 | ) 22 | 23 | type ( 24 | Mounts []Mount 25 | 26 | Mount struct { 27 | Destination types.String `tfsdk:"destination"` 28 | 29 | Volume *MountVolume `tfsdk:"volume"` 30 | Bind *MountBind `tfsdk:"bind"` 31 | // Tmpfs *MountTmpfs `tfsdk:"tmpfs"` 32 | } 33 | 34 | // MountVolume mounts a named volume 35 | MountVolume struct { 36 | Name types.String `tfsdk:"name"` 37 | 38 | ReadOnly types.Bool `tfsdk:"read_only"` 39 | Chown types.Bool `tfsdk:"chown"` 40 | 41 | Suid types.Bool `tfsdk:"suid"` 42 | Exec types.Bool `tfsdk:"exec"` 43 | Dev types.Bool `tfsdk:"dev"` 44 | IDmap types.Bool `tfsdk:"idmap"` 45 | } 46 | 47 | // MountBind mounts host path 48 | MountBind struct { 49 | Path types.String `tfsdk:"path"` 50 | 51 | ReadOnly types.Bool `tfsdk:"read_only"` 52 | Chown types.Bool `tfsdk:"chown"` 53 | 54 | Suid types.Bool `tfsdk:"suid"` 55 | Exec types.Bool `tfsdk:"exec"` 56 | Dev types.Bool `tfsdk:"dev"` 57 | IDmap types.Bool `tfsdk:"idmap"` 58 | 59 | Propagation types.String `tfsdk:"propagation"` 60 | Recursive types.Bool `tfsdk:"recursive"` 61 | Relabel types.Bool `tfsdk:"relabel"` 62 | } 63 | 64 | // MountTmpfs mounts a tmpfs 65 | MountTmpfs struct { 66 | ReadOnly types.Bool `tfsdk:"read_only"` 67 | Chown types.Bool `tfsdk:"chown"` 68 | 69 | Suid types.Bool `tfsdk:"suid"` 70 | Exec types.Bool `tfsdk:"exec"` 71 | Dev types.Bool `tfsdk:"dev"` 72 | 73 | Size types.String `tfsdk:"size"` 74 | Mode types.String `tfsdk:"mode"` 75 | TmpCopyUp types.Bool `tfsdk:"tmpcopyup"` 76 | } 77 | ) 78 | 79 | func (m Mounts) GetSchema(ctx context.Context) schema.Attribute { 80 | return schema.SetNestedAttribute{ 81 | Description: "Mounts volume, bind, image, tmpfs, etc..", 82 | Required: false, 83 | Optional: true, 84 | Computed: true, 85 | PlanModifiers: []planmodifier.Set{ 86 | modifier.RequiresReplaceComputed(), 87 | }, 88 | NestedObject: schema.NestedAttributeObject{ 89 | Attributes: map[string]schema.Attribute{ 90 | "destination": schema.StringAttribute{ 91 | Description: "Target path", 92 | Required: true, 93 | Computed: false, 94 | Validators: []validator.String{ 95 | //TODO 96 | }, 97 | }, 98 | "volume": schema.SingleNestedAttribute{ 99 | Description: "Named Volume", 100 | Optional: true, 101 | Computed: false, 102 | Validators: []validator.Object{ 103 | objectvalidator.ConflictsWith( 104 | path.MatchRelative().AtParent().AtName("bind"), 105 | ), 106 | }, 107 | PlanModifiers: []planmodifier.Object{ 108 | objectplanmodifier.RequiresReplace(), 109 | }, 110 | Attributes: map[string]schema.Attribute{ 111 | "name": schema.StringAttribute{ 112 | Description: "Name of the volume", 113 | Required: true, 114 | Computed: false, 115 | Validators: []validator.String{ 116 | validators.MatchName(), 117 | }, 118 | PlanModifiers: []planmodifier.String{ 119 | stringplanmodifier.RequiresReplace(), 120 | }, 121 | }, 122 | "read_only": m.attributeSchemaReadOnly(), 123 | "dev": m.attributeSchemaDev(), 124 | "exec": m.attributeSchemaExec(), 125 | "suid": m.attributeSchemaSuid(), 126 | "chown": m.attributeSchemaChown(), 127 | "idmap": m.attributeSchemaIDmap(), 128 | }, 129 | }, 130 | 131 | "bind": schema.SingleNestedAttribute{ 132 | Description: "Bind Volume", 133 | Optional: true, 134 | Computed: false, 135 | Validators: []validator.Object{ 136 | objectvalidator.ConflictsWith( 137 | path.MatchRelative().AtParent().AtName("volume"), 138 | ), 139 | }, 140 | PlanModifiers: []planmodifier.Object{ 141 | objectplanmodifier.RequiresReplace(), 142 | }, 143 | Attributes: map[string]schema.Attribute{ 144 | "path": schema.StringAttribute{ 145 | Description: "Host path", 146 | Required: true, 147 | Computed: false, 148 | Validators: []validator.String{ 149 | // TODO 150 | }, 151 | PlanModifiers: []planmodifier.String{ 152 | stringplanmodifier.RequiresReplace(), 153 | }, 154 | }, 155 | "read_only": m.attributeSchemaReadOnly(), 156 | "dev": m.attributeSchemaDev(), 157 | "exec": m.attributeSchemaExec(), 158 | "suid": m.attributeSchemaSuid(), 159 | "chown": m.attributeSchemaChown(), 160 | "idmap": m.attributeSchemaIDmap(), 161 | "propagation": m.attributeSchemaBindPropagation(), 162 | "recursive": m.attributeSchemaBindRecursive(), 163 | "relabel": m.attributeSchemaBindRelabel(), 164 | }, 165 | }, 166 | }, 167 | }, 168 | } 169 | 170 | // TODO: 171 | // While its technically possible, the podman api does not support this case for pods pretty well. 172 | // The tmpfs mount will be created on the infra container, but it is not exposed on inspect anymore 173 | // https://github.com/containers/podman/blob/v4.3.1/libpod/container_inspect.go#L276-L280 174 | // "tmpfs": { 175 | // Description: "Tmpfs Volume", 176 | // Optional: true, 177 | // Computed: false, 178 | // Attributes: tfsdk.SingleNestedAttributes( 179 | // map[string]tfsdk.Attribute{ 180 | // "read_only": m.attributeSchemaReadOnly(), 181 | // "dev": m.attributeSchemaDev(), 182 | // "exec": m.attributeSchemaExec(), 183 | // "suid": m.attributeSchemaSuid(), 184 | // "chown": m.attributeSchemaChown(), 185 | // "size": m.attributeSchemaTmpfsSize(), 186 | // "mode": m.attributeSchemaTmpfsMode(), 187 | // "tmpcopyup": m.attributeSchemaTmpfsTmpCopyUp(), 188 | // }, 189 | // ), 190 | // }, 191 | 192 | } 193 | 194 | // ToPodmanSpec creates volume and mounts 195 | func (m Mounts) ToPodmanSpec(diags *diag.Diagnostics) ([]*specgen.NamedVolume, []specs.Mount) { 196 | 197 | specNamedVolumes := make([]*specgen.NamedVolume, 0) 198 | specMounts := make([]specs.Mount, 0) 199 | for _, mount := range m { 200 | if mount.Volume != nil { 201 | // Named volume mount options 202 | specVol := specgen.NamedVolume{ 203 | Name: mount.Volume.Name.ValueString(), 204 | Dest: mount.Destination.ValueString(), 205 | } 206 | 207 | specVol.Options = appendMountOptBool(specVol.Options, mount.Volume.ReadOnly, "ro", "rw") 208 | specVol.Options = appendMountOptBool(specVol.Options, mount.Volume.Dev, "dev", "nodev") 209 | specVol.Options = appendMountOptBool(specVol.Options, mount.Volume.Exec, "exec", "noexec") 210 | specVol.Options = appendMountOptBool(specVol.Options, mount.Volume.Suid, "sui", "nosuid") 211 | 212 | if !mount.Volume.Chown.IsNull() && mount.Volume.Chown.ValueBool() { 213 | specVol.Options = append(specVol.Options, "U") 214 | } 215 | 216 | if !mount.Volume.IDmap.IsNull() && mount.Volume.IDmap.ValueBool() { 217 | specVol.Options = append(specVol.Options, "idmap") 218 | } 219 | 220 | specNamedVolumes = append(specNamedVolumes, &specVol) 221 | 222 | } else if mount.Bind != nil { 223 | // Bind mount options 224 | specMount := specs.Mount{ 225 | Destination: mount.Destination.ValueString(), 226 | Type: "bind", 227 | Source: mount.Bind.Path.ValueString(), 228 | } 229 | 230 | specMount.Options = appendMountOptBool(specMount.Options, mount.Bind.ReadOnly, "ro", "rw") 231 | specMount.Options = appendMountOptBool(specMount.Options, mount.Bind.Dev, "dev", "nodev") 232 | specMount.Options = appendMountOptBool(specMount.Options, mount.Bind.Exec, "exec", "noexec") 233 | specMount.Options = appendMountOptBool(specMount.Options, mount.Bind.Suid, "suid", "nosuid") 234 | 235 | if mount.Bind.Chown.ValueBool() { 236 | specMount.Options = append(specMount.Options, "U") 237 | } 238 | 239 | if mount.Bind.IDmap.ValueBool() { 240 | specMount.Options = append(specMount.Options, "idmap") 241 | } 242 | 243 | if mount.Bind.Propagation.ValueString() != "" { 244 | specMount.Options = append(specMount.Options, mount.Bind.Propagation.ValueString()) 245 | } 246 | specMount.Options = appendMountOptBool(specMount.Options, mount.Bind.Recursive, "rbind", "bind") 247 | // public = z, private = Z 248 | specMount.Options = appendMountOptBool(specMount.Options, mount.Bind.Relabel, "z", "Z") 249 | 250 | specMounts = append(specMounts, specMount) 251 | } 252 | // else if mount.Tmpfs != nil { 253 | // 254 | // // Tmpfs mount options 255 | // specMount := specs.Mount{ 256 | // Destination: mount.Destination.ValueString(), 257 | // Type: "tmpfs", 258 | // } 259 | // 260 | // specMount.Options = appendMountOptBool(specMount.Options, mount.Tmpfs.ReadOnly, "ro", "rw") 261 | // specMount.Options = appendMountOptBool(specMount.Options, mount.Tmpfs.Dev, "dev", "nodev") 262 | // specMount.Options = appendMountOptBool(specMount.Options, mount.Tmpfs.Exec, "exec", "noexec") 263 | // specMount.Options = appendMountOptBool(specMount.Options, mount.Tmpfs.Suid, "suid", "nosuid") 264 | // 265 | // if mount.Tmpfs.Chown.ValueBool() { 266 | // specMount.Options = append(specMount.Options, "U") 267 | // } 268 | // 269 | // specMounts = append(specMounts, specMount) 270 | // } 271 | } 272 | return specNamedVolumes, specMounts 273 | } 274 | 275 | func FromPodmanToMounts(diags *diag.Diagnostics, specMounts []define.InspectMount) Mounts { 276 | mounts := make(Mounts, 0) 277 | 278 | for _, specMount := range specMounts { 279 | opts := parseMountOptions(diags, specMount.Options) 280 | if opts.readOnly.IsNull() { 281 | opts.readOnly = types.BoolValue(!specMount.RW) 282 | } 283 | 284 | switch specMount.Type { 285 | case "volume": 286 | mounts = append(mounts, Mount{ 287 | Destination: types.StringValue(specMount.Destination), 288 | Volume: &MountVolume{ 289 | Name: types.StringValue(specMount.Name), 290 | ReadOnly: opts.readOnly, 291 | Dev: opts.dev, 292 | Exec: opts.exec, 293 | Suid: opts.suid, 294 | Chown: opts.chown, 295 | IDmap: opts.idmap, 296 | }, 297 | }) 298 | 299 | case "bind": 300 | mounts = append(mounts, Mount{ 301 | Destination: types.StringValue(specMount.Destination), 302 | Bind: &MountBind{ 303 | Path: types.StringValue(specMount.Source), 304 | ReadOnly: opts.readOnly, 305 | Dev: opts.dev, 306 | Exec: opts.exec, 307 | Suid: opts.suid, 308 | Chown: opts.chown, 309 | IDmap: opts.idmap, 310 | Propagation: opts.propagation, 311 | Recursive: opts.recursive, 312 | Relabel: opts.relabel, 313 | }, 314 | }) 315 | 316 | // case "tmpfs": 317 | // mounts = append(mounts, Mount{ 318 | // Destination: types.String{Value: specMount.Destination}, 319 | // Tmpfs: &MountTmpfs{ 320 | // ReadOnly: opts.readOnly, 321 | // Dev: opts.dev, 322 | // Exec: opts.exec, 323 | // Suid: opts.suid, 324 | // Chown: opts.chown, 325 | // Size: opts.size, 326 | // }, 327 | // }) 328 | 329 | default: 330 | diags.AddError("Unknown mount type retrieved", specMount.Type) 331 | } 332 | } 333 | if len(mounts) == 0 { 334 | return nil 335 | } 336 | return mounts 337 | } 338 | 339 | // appendMountOptBool appends a mapped boolen value 340 | func appendMountOptBool(opts []string, v types.Bool, trueVal string, falseVal string) []string { 341 | if !v.IsNull() { 342 | if v.ValueBool() { 343 | opts = append(opts, trueVal) 344 | } else { 345 | opts = append(opts, falseVal) 346 | } 347 | } 348 | return opts 349 | } 350 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------