Support for Terraforming Fleet Teams (#18750)

This project adds support for terraforming teams in Fleet. If you have
100+ teams, managing them is is prone to error, mistakes, lost
configuration, and general pain. An industry standard tool like
terraform can unify this configuration as code.

In order to do this, I wrote a terraform provider that on one end talks
to the Fleet api, and on the other end implements an interface for
terraform. More information is in the README.

A small sample `main.tf` file is supplied.

---------

Co-authored-by: Brock Walters <153771548+nonpunctual@users.noreply.github.com>
This commit is contained in:
Mike Yoder 2024-06-20 12:47:35 -07:00 committed by GitHub
parent 03dd4721b6
commit c7ea0125d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 2318 additions and 0 deletions

2
tools/terraform/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
provider_code_spec.json
tf/terraformrc-dev-override

42
tools/terraform/Makefile Normal file
View file

@ -0,0 +1,42 @@
#! /usr/bin/env make
#
# While not very elegant as far as Makefiles go, this Makefile does
# contain the basic commands to get you terraforming your FleetDM
# teams. See the README for details.
provider_code_spec.json: openapi.json
tfplugingen-openapi generate --config generator.yaml --output ./provider_code_spec.json ./openapi.json
provider/team_resource_gen.go: provider_code_spec.json
tfplugingen-framework generate resources --input provider_code_spec.json --output ./provider --package provider
.PHONY: install build test tidy gen plan apply
gen: provider/team_resource_gen.go
install: gen
go install ./...
build: gen
go build ./...
test: gen
@test -n "$(FLEETDM_APIKEY)" || (echo "FLEETDM_APIKEY is not set" && exit 1)
FLEETDM_URL='https://rbx.cloud.fleetdm.com' TF_ACC=1 go test ./...
tidy:
go mod tidy
plan: tf/terraformrc-dev-override
cd tf && TF_CLI_CONFIG_FILE=./terraformrc-dev-override terraform plan
apply: tf/terraformrc-dev-override
cd tf && TF_CLI_CONFIG_FILE=./terraformrc-dev-override terraform apply -auto-approve
tf/terraformrc-dev-override:
@echo "provider_installation { \\n\
dev_overrides { \\n\
\"fleetdm.com/tf/fleetdm\" = \"$$HOME/go/bin\" \\n\
} \\n\
direct {} \\n\
}" > $@

61
tools/terraform/README.md Normal file
View file

@ -0,0 +1,61 @@
# Terraform Provider for FleetDM Teams
This is a Terraform provider for managing FleetDM teams. When you have
100+ teams in FleetDM, and manually managing them is not feasible. The
primary setting of concern is the team's "agent options" which
consists of some settings and command line flags. These (potentially
dangerously) configure FleetDM all machines.
## Usage
All the interesting commands are in the Makefile. If you just want
to use the thing, see `make install` and `make apply`.
Note that if you run `terraform apply` in the `tf` directory, it won't
work out of the box. That's because you need to set the
`TF_CLI_CONFIG_FILE` environment variable to point to a file that
enables local development of this provider. The Makefile does this
for you.
Future work: actually publish this provider.
## Development
### Code Generation
See `make gen`. It will create team_resource_gen.go, which defines
the types that Terraform knows about. This is automatically run
when you run `make install`.
### Running locally
See `make plan` and `make apply`.
### Running Tests
You probably guessed this. See `make test`. Note that these tests
require a FleetDM server to be running. The tests will create teams
and delete them when they're done. The tests also require a valid
FleetDM API token to be in the `FLEETDM_APIKEY` environment variable.
### Debugging locally
The basic idea is that you want to run the provider in a debugger.
When terraform normally runs, it will execute the provider a few
times in the course of operations. What you want to do instead is
to run the provider in debug mode and tell terraform to contact it.
To do this, you need to start the provider with the `-debug` flag
inside a debugger. You'll also need to give it the FLEETDM_APIKEY
environment variable. The provider will print out a big environment
variable that you can copy and paste to your command line.
When you run `terraform apply` or the like, you'll invoke it with
that big environment variable. It'll look something like
```shell
TF_REATTACH_PROVIDERS='{"fleetdm.com/tf/fleetdm":{"Protocol":"grpc","ProtocolVersion":6,"Pid":33644,"Test":true,"Addr":{"Network":"unix","String":"/var/folders/32/xw2p1jtd4w10hpnsyrb_4nmm0000gq/T/plugin771405263"}}}' terraform apply
```
With this magic, terraform will look to your provider that's running
in a debugger. You get breakpoints and the goodness of a debugger.

View file

@ -0,0 +1,301 @@
package fleetdm_client
// This file gives us a nice API to use to call FleetDM's API. It's focused
// only on teams.
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const prefix = "/api/v1/fleet"
const teamPrefix = prefix + "/teams"
type Team struct {
Name string `json:"name"`
ID int64 `json:"id"`
Description string `json:"description"`
AgentOptions interface{} `json:"agent_options"`
Scripts interface{} `json:"scripts"`
Secrets []struct {
Secret string `json:"secret"`
Created time.Time `json:"created_at"`
TeamID int `json:"team_id"`
} `json:"secrets"`
}
type TeamCreate struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
}
type TeamGetResponse struct {
Team Team `json:"team"`
}
type TeamQueryResponse struct {
Teams []Team `json:"teams"`
}
// FleetDMClient is a FleetDM HTTP client that overrides http.DefaultClient.
type FleetDMClient struct {
*http.Client
URL string
APIKey string
}
// NewFleetDMClient creates a new instance of FleetDMClient with the provided
// URL and API key.
func NewFleetDMClient(url, apiKey string) *FleetDMClient {
// Ensure the URL ends with a trailing slash for bizarre parsing reasons
if !strings.HasSuffix(url, "/") {
url += "/"
}
return &FleetDMClient{
Client: http.DefaultClient,
URL: url,
APIKey: apiKey,
}
}
// Do will add necessary headers and call the http.Client.Do method.
func (c *FleetDMClient) do(req *http.Request, query *url.Values) (*http.Response, error) {
// Add the API key to the request header
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
req.Header.Add("Accept", `application/json`)
// Set the request URL based on the client URL
baseURL, err := url.Parse(c.URL)
if err != nil {
return nil, fmt.Errorf("failed to parse base URL %s: %w", c.URL, err)
}
req.URL = baseURL.JoinPath(req.URL.Path)
if query != nil {
req.URL.RawQuery = query.Encode()
}
// Send the request using the embedded http.Client
return c.Client.Do(req)
}
// TeamNameToID will return the ID of a team given the name.
func (c *FleetDMClient) TeamNameToID(name string) (int64, error) {
req, err := http.NewRequest(http.MethodGet, teamPrefix, nil)
if err != nil {
return 0, fmt.Errorf("failed to create GET request for %s: %w", teamPrefix, err)
}
vals := make(url.Values)
vals.Add("query", name)
resp, err := c.do(req, &vals)
if err != nil {
return 0, fmt.Errorf("failed to GET %s %s: %w", teamPrefix, name, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("failed to get team: %s %s", name, resp.Status)
}
var teamqry TeamQueryResponse
b, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read team response: %w", err)
}
err = json.Unmarshal(b, &teamqry)
if err != nil {
return 0, fmt.Errorf("failed to decode get team response: %w", err)
}
for _, team := range teamqry.Teams {
if team.Name == name {
return team.ID, nil
}
}
return 0, fmt.Errorf("team %s not found", name)
}
// CreateTeam creates a new team with the provided name and description.
func (c *FleetDMClient) CreateTeam(name string, description string) (*TeamGetResponse, error) {
teamCreate := TeamCreate{
Name: name,
Description: description,
}
nameJson, err := json.Marshal(teamCreate)
if err != nil {
return nil, fmt.Errorf("failed to create team body: %w", err)
}
req, err := http.NewRequest(http.MethodPost, teamPrefix, bytes.NewReader(nameJson))
if err != nil {
return nil, fmt.Errorf("failed to create POST request for %s name %s: %w",
req.URL.String(), name, err)
}
resp, err := c.do(req, nil)
if err != nil {
return nil, fmt.Errorf("failed to POST %s name %s: %w",
req.URL.String(), name, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to create team %s: %s %s", name, resp.Status, string(b))
}
var newTeam TeamGetResponse
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read team response: %w", err)
}
err = json.Unmarshal(b, &newTeam)
if err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &newTeam, nil
}
// GetTeam returns the team with the provided ID.
func (c *FleetDMClient) GetTeam(id int64) (*TeamGetResponse, error) {
url := teamPrefix + "/" + strconv.FormatInt(id, 10)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create GET request for %s: %w",
url, err)
}
resp, err := c.do(req, nil)
if err != nil {
return nil, fmt.Errorf("failed to GET %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get team %d: %s %s", id, resp.Status, string(b))
}
var team TeamGetResponse
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read team response: %w", err)
}
err = json.Unmarshal(b, &team)
if err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &team, nil
}
// UpdateTeam updates the team with the provided ID with the provided name and description.
func (c *FleetDMClient) UpdateTeam(id int64, name, description *string) (*TeamGetResponse, error) {
if name == nil && description == nil {
return nil, fmt.Errorf("both name and description are nil")
}
url := teamPrefix + "/" + strconv.FormatInt(id, 10)
var teamUpdate TeamCreate
if name != nil {
teamUpdate.Name = *name
}
if description != nil {
teamUpdate.Description = *description
}
updateJson, err := json.Marshal(teamUpdate)
if err != nil {
return nil, fmt.Errorf("failed to update team body request: %w", err)
}
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader(updateJson))
if err != nil {
return nil, fmt.Errorf("failed to create PATCH request for %s body %s: %w",
url, updateJson, err)
}
resp, err := c.do(req, nil)
if err != nil {
return nil, fmt.Errorf("failed to PATCH %s body %s: %w",
url, updateJson, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed PATCH %s body %s: %s %s",
url, updateJson, resp.Status, string(b))
}
var newTeam TeamGetResponse
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read team response: %w", err)
}
err = json.Unmarshal(b, &newTeam)
if err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &newTeam, nil
}
// UpdateAgentOptions pretends that the agent options are a string, when it's really actually json.
// Strangely the body that comes back is a team, not just the agent options.
func (c *FleetDMClient) UpdateAgentOptions(id int64, ao string) (*TeamGetResponse, error) {
// First verify it's actually json.
valid := json.Valid([]byte(ao))
if !valid {
return nil, fmt.Errorf("agent_options isn't json: %s", ao)
}
aoUrl := teamPrefix + "/" + strconv.FormatInt(id, 10) + "/" + "agent_options"
req, err := http.NewRequest(http.MethodPost, aoUrl, strings.NewReader(ao))
if err != nil {
return nil, fmt.Errorf("failed to create agent_options POST request for %s id %d: %w",
teamPrefix, id, err)
}
resp, err := c.do(req, nil)
if err != nil {
return nil, fmt.Errorf("failed to POST agent_options %s id %d: %w",
teamPrefix, id, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to modify agent_options %d: %s %s", id, resp.Status, string(b))
}
var team TeamGetResponse
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read team response: %w", err)
}
err = json.Unmarshal(b, &team)
if err != nil {
return nil, fmt.Errorf("failed to decode agent_options team response: %w", err)
}
return &team, nil
}
// DeleteTeam deletes the team with the provided ID.
func (c *FleetDMClient) DeleteTeam(id int64) error {
url := teamPrefix + "/" + strconv.FormatInt(id, 10)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("failed to create DELETE request for %s: %w", url, err)
}
resp, err := c.do(req, nil)
if err != nil {
return fmt.Errorf("failed to DELETE %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("failed to delete team %d: %s", id, resp.Status)
}
return nil
}

View file

@ -0,0 +1,93 @@
package fleetdm_client
import (
"encoding/json"
"github.com/stretchr/testify/require"
"math/rand"
"os"
"testing"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyz")
var defaultDescription = "Awesome description"
var defaultAgentOptions = `{"config":{"decorators":{"load":["SELECT uuid AS host_uuid FROM system_info;","SELECT hostname AS hostname FROM system_info;"]},"options":{"disable_distributed":false,"distributed_interval":10,"distributed_plugin":"tls","distributed_tls_max_attempts":3,"logger_tls_endpoint":"/api/osquery/log","logger_tls_period":10,"pack_delimiter":"/"}},"overrides":{}}`
func randTeam() string {
b := make([]rune, 6)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return "aaa-" + string(b)
}
func TestBasic(t *testing.T) {
apiKey := os.Getenv("FLEETDM_APIKEY")
if apiKey == "" {
t.Skip("FLEETDM_APIKEY not set")
}
apiURL := os.Getenv("FLEETDM_URL")
if apiURL == "" {
t.Skip("FLEETDM_URL not set")
}
client := NewFleetDMClient(apiURL, apiKey)
require.NotNil(t, client)
// Create a nice default team
teamName := randTeam()
team, err := client.CreateTeam(teamName, defaultDescription)
require.NoError(t, err)
require.NotNil(t, team)
require.Equal(t, teamName, team.Team.Name)
require.Equal(t, defaultDescription, team.Team.Description)
aoBytes, err := json.Marshal(team.Team.AgentOptions)
require.NoError(t, err)
require.Equal(t, defaultAgentOptions, string(aoBytes))
// And verify we can get that team back
teamGotten, err := client.GetTeam(team.Team.ID)
require.NoError(t, err)
require.NotNil(t, teamGotten)
require.Equal(t, team.Team.ID, teamGotten.Team.ID)
require.Equal(t, team.Team.Name, teamGotten.Team.Name)
require.Equal(t, teamName, teamGotten.Team.Name)
require.Equal(t, defaultDescription, teamGotten.Team.Description)
aoBytes, err = json.Marshal(teamGotten.Team.AgentOptions)
require.NoError(t, err)
require.Equal(t, defaultAgentOptions, string(aoBytes))
// Set different agent options
newAO := `{"command_line_flags":{"disable_events":true},"config":{"options":{"logger_tls_endpoint":"/test"}},"overrides":{}}`
teamAO, err := client.UpdateAgentOptions(team.Team.ID, newAO)
require.NoError(t, err)
require.NotNil(t, teamAO)
require.Equal(t, team.Team.ID, teamAO.Team.ID)
aoBytes, err = json.Marshal(teamAO.Team.AgentOptions)
require.NoError(t, err)
require.Equal(t, newAO, string(aoBytes))
// Set a different description and different name
newName := randTeam()
newDescription := "New description"
teamUp, err := client.UpdateTeam(team.Team.ID, &newName, &newDescription)
require.NoError(t, err)
require.NotNil(t, teamUp)
require.Equal(t, team.Team.ID, teamUp.Team.ID)
require.Equal(t, newName, teamUp.Team.Name)
require.Equal(t, newDescription, teamUp.Team.Description)
// Lookup the team based on the new name
teamId, err := client.TeamNameToID(newName)
require.NoError(t, err)
require.NotEqual(t, 0, teamId)
require.Equal(t, teamUp.Team.ID, teamId)
require.Equal(t, newName, teamUp.Team.Name)
// And finally, delete the team
err = client.DeleteTeam(team.Team.ID)
require.NoError(t, err)
// And verify it's gone
teamDNE, err := client.GetTeam(team.Team.ID)
require.Error(t, err)
require.Nil(t, teamDNE)
}

View file

@ -0,0 +1,23 @@
provider:
name: fleetdm_teams
schema_ref: '#/components/schemas/Team'
resources:
team:
create:
path: /api/v1/fleet/teams
method: POST
read:
path: /api/v1/fleet/teams/{id}
method: GET
update:
path: /api/v1/fleet/teams/{id}
method: PUT
delete:
path: /api/v1/fleet/teams/{id}
method: DELETE
schema:
attributes:
overrides:
name:
description: Team name

66
tools/terraform/go.mod Normal file
View file

@ -0,0 +1,66 @@
module terraform-provider-fleetdm
go 1.22
require (
github.com/hashicorp/terraform-plugin-framework v1.7.0
github.com/hashicorp/terraform-plugin-go v0.22.1
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.7.0
github.com/stretchr/testify v1.8.1
)
require (
github.com/ProtonMail/go-crypto v1.1.0-alpha.0 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.6.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.6.3 // indirect
github.com/hashicorp/hcl/v2 v2.20.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.20.0 // indirect
github.com/hashicorp/terraform-json v0.21.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.14.3 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

228
tools/terraform/go.sum Normal file
View file

@ -0,0 +1,228 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE9N5vPhgY2I+j0=
github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs=
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hc-install v0.6.3 h1:yE/r1yJvWbtrJ0STwScgEnCanb0U9v7zp0Gbkmcoxqs=
github.com/hashicorp/hc-install v0.6.3/go.mod h1:KamGdbodYzlufbWh4r9NRo8y6GLHWZP2GBtdnms1Ln0=
github.com/hashicorp/hcl/v2 v2.20.0 h1:l++cRs/5jQOiKVvqXZm/P1ZEfVXJmvLS9WSVxkaeTb4=
github.com/hashicorp/hcl/v2 v2.20.0/go.mod h1:WmcD/Ym72MDOOx5F62Ly+leloeu6H7m0pG7VBiU6pQk=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo=
github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw=
github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U=
github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
github.com/hashicorp/terraform-plugin-framework v1.7.0 h1:wOULbVmfONnJo9iq7/q+iBOBJul5vRovaYJIu2cY/Pw=
github.com/hashicorp/terraform-plugin-framework v1.7.0/go.mod h1:jY9Id+3KbZ17OMpulgnWLSfwxNVYSoYBQFTgsx044CI=
github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w=
github.com/hashicorp/terraform-plugin-go v0.22.1/go.mod h1:qrjnqRghvQ6KnDbB12XeZ4FluclYwptntoWCr9QaXTI=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 h1:qHprzXy/As0rxedphECBEQAh3R4yp6pKksKHcqZx5G8=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0/go.mod h1:H+8tjs9TjV2w57QFVSMBQacf8k/E1XwLXGCARgViC6A=
github.com/hashicorp/terraform-plugin-testing v1.7.0 h1:I6aeCyZ30z4NiI3tzyDoO6fS7YxP5xSL1ceOon3gTe8=
github.com/hashicorp/terraform-plugin-testing v1.7.0/go.mod h1:sbAreCleJNOCz+y5vVHV8EJkIWZKi/t4ndKiUjM9vao=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho=
github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

52
tools/terraform/main.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"terraform-provider-fleetdm/provider"
)
// Run "go generate" to format example terraform files and generate the docs for the registry/website
// If you do not have terraform installed, you can remove the formatting command, but its suggested to
// ensure the documentation is formatted properly.
//go:generate terraform fmt -recursive ./examples/
// Run the docs generation tool, check its repository for more information on how it works and how docs
// can be customized.
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate -provider-name scaffolding
var (
// these will be set by the goreleaser configuration
// to appropriate values for the compiled binary.
version string = "dev"
// goreleaser can pass other information to the main package, such as the specific commit
// https://goreleaser.com/cookbooks/using-main.version/
)
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false,
"set to true to run the provider with support for debuggers like delve")
flag.Parse()
opts := providerserver.ServeOpts{
// TODO: Update this string with the published name of your provider.
// Also update the tfplugindocs generate command to either remove the
// -provider-name flag or set its value to the updated provider name.
//Address: "registry.terraform.io/hashicorp/scaffolding",
Address: "fleetdm.com/tf/fleetdm",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}

View file

@ -0,0 +1,210 @@
{
"openapi": "3.0.0",
"info": {
"title": "Fleet Premium API",
"description": "API for managing teams and related functionalities in Fleet Premium",
"version": "1.0.0"
},
"servers": [
{
"url": "https://rbx.cloud.fleetdm.com/"
}
],
"paths": {
"/api/v1/fleet/teams": {
"get": {
"summary": "List teams",
"parameters": [
{
"name": "page",
"in": "query",
"description": "Page number of the results to fetch.",
"schema": {
"type": "integer"
}
},
{
"name": "per_page",
"in": "query",
"description": "Results per page.",
"schema": {
"type": "integer"
}
},
{
"name": "order_key",
"in": "query",
"description": "What to order results by. Can be any column in the teams table.",
"schema": {
"type": "string"
}
},
{
"name": "order_direction",
"in": "query",
"description": "The direction of the order given the order key. Options include asc and desc. Default is asc.",
"schema": {
"type": "string",
"enum": ["asc", "desc"]
}
},
{
"name": "query",
"in": "query",
"description": "Search query keywords. Searchable fields include name.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "List of teams",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"teams": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Team"
}
}
}
}
}
}
}
}
},
"post": {
"summary": "Create team",
"requestBody": {
"description": "Team details",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TeamCreateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Team created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
}
}
}
},
"/api/v1/fleet/teams/{id}": {
"get": {
"summary": "Get team",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The desired team's ID.",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Team details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
}
}
},
"delete": {
"summary": "Delete team",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "The desired team's ID.",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Team deleted successfully"
}
}
}
}
},
"components": {
"schemas": {
"Team": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"agent_options": {
"type": "string"
},
"secrets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"secret": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"team_id": {
"type": "integer"
}
}
}
}
}
},
"TeamCreateRequest": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"agent_options": {
"type": "string"
}
},
"required": ["name"]
}
}
}
}

View file

@ -0,0 +1,146 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-log/tflog"
"os"
"terraform-provider-fleetdm/fleetdm_client"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// This Terraform provider is based on the example provider from the Terraform
// documentation. It is a simple provider that interacts with the FleetDM API.
// Ensure FleetDMProvider satisfies various provider interfaces.
var _ provider.Provider = &FleetDMProvider{}
var _ provider.ProviderWithFunctions = &FleetDMProvider{}
// FleetDMProvider defines the provider implementation.
type FleetDMProvider struct {
// version is set to the provider version on release, "dev" when the
// provider is built and ran locally, and "test" when running acceptance
// testing.
version string
}
// FleetDMProviderModel describes the provider data model. It requires a URL
// and api key to communicate with FleetDM.
type FleetDMProviderModel struct {
Url types.String `tfsdk:"url"`
ApiKey types.String `tfsdk:"apikey"`
}
func (p *FleetDMProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "fleetdm"
resp.Version = p.version
}
func (p *FleetDMProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"url": schema.StringAttribute{
MarkdownDescription: "URL of your FleetDM server",
Optional: true,
},
"apikey": schema.StringAttribute{
MarkdownDescription: "API Key for authentication",
Optional: true,
Sensitive: true,
},
},
}
}
func (p *FleetDMProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config FleetDMProviderModel
tflog.Info(ctx, "Configuring FleetDM client")
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if config.Url.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("url"),
"Unknown FleetDM url",
"Url is unknown")
}
if config.ApiKey.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("apikey"),
"Unknown FleetDM apikey",
"api key is unknown")
}
if resp.Diagnostics.HasError() {
return
}
url := os.Getenv("FLEETDM_URL")
apikey := os.Getenv("FLEETDM_APIKEY")
if !config.Url.IsNull() {
url = config.Url.ValueString()
}
if !config.ApiKey.IsNull() {
apikey = config.ApiKey.ValueString()
}
if url == "" {
resp.Diagnostics.AddAttributeError(
path.Root("url"),
"Missing url",
"Really, the url is required")
}
if apikey == "" {
resp.Diagnostics.AddAttributeError(
path.Root("apikey"),
"Missing apikey",
"Really, the apikey is required")
}
if resp.Diagnostics.HasError() {
return
}
// Example client configuration for data sources and resources
client := fleetdm_client.NewFleetDMClient(url, apikey)
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *FleetDMProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewTeamsResource,
}
}
func (p *FleetDMProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{}
}
func (p *FleetDMProvider) Functions(ctx context.Context) []func() function.Function {
return []func() function.Function{}
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &FleetDMProvider{
version: version,
}
}
}

View file

@ -0,0 +1,33 @@
package provider
import (
"os"
"testing"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
)
// testAccProtoV6ProviderFactories are used to instantiate a provider during
// acceptance testing. The factory function will be invoked for every Terraform
// CLI command executed to create a provider server to which the CLI can
// reattach.
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"fleetdm": providerserver.NewProtocol6WithError(New("test")()),
}
func testAccPreCheck(t *testing.T) {
// You can add code here to run prior to any test case execution, for example assertions
// about the appropriate environment variables being set are common to see in a pre-check
// function.
apiKey := os.Getenv("FLEETDM_APIKEY")
if apiKey == "" {
t.Skip("FLEETDM_APIKEY not set")
}
// I don't like this, but I can't figure out how to pass the url otherwise...
url := os.Getenv("FLEETDM_URL")
if url == "" {
t.Skip("FLEETDM_URL not set")
}
}

View file

@ -0,0 +1,494 @@
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
func TeamResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"agent_options": schema.StringAttribute{
Optional: true,
Computed: true,
},
"description": schema.StringAttribute{
Optional: true,
Computed: true,
},
"id": schema.Int64Attribute{
Computed: true,
Description: "The desired team's ID.",
MarkdownDescription: "The desired team's ID.",
},
"name": schema.StringAttribute{
Required: true,
Description: "Team name",
MarkdownDescription: "Team name",
},
"secrets": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"created_at": schema.StringAttribute{
Computed: true,
},
"secret": schema.StringAttribute{
Computed: true,
},
"team_id": schema.Int64Attribute{
Computed: true,
},
},
CustomType: SecretsType{
ObjectType: types.ObjectType{
AttrTypes: SecretsValue{}.AttributeTypes(ctx),
},
},
},
Computed: true,
},
},
}
}
type TeamModel struct {
AgentOptions types.String `tfsdk:"agent_options"`
Description types.String `tfsdk:"description"`
Id types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Secrets types.List `tfsdk:"secrets"`
}
var _ basetypes.ObjectTypable = SecretsType{}
type SecretsType struct {
basetypes.ObjectType
}
func (t SecretsType) Equal(o attr.Type) bool {
other, ok := o.(SecretsType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t SecretsType) String() string {
return "SecretsType"
}
func (t SecretsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
createdAtAttribute, ok := attributes["created_at"]
if !ok {
diags.AddError(
"Attribute Missing",
`created_at is missing from object`)
return nil, diags
}
createdAtVal, ok := createdAtAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`created_at expected to be basetypes.StringValue, was: %T`, createdAtAttribute))
}
secretAttribute, ok := attributes["secret"]
if !ok {
diags.AddError(
"Attribute Missing",
`secret is missing from object`)
return nil, diags
}
secretVal, ok := secretAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`secret expected to be basetypes.StringValue, was: %T`, secretAttribute))
}
teamIdAttribute, ok := attributes["team_id"]
if !ok {
diags.AddError(
"Attribute Missing",
`team_id is missing from object`)
return nil, diags
}
teamIdVal, ok := teamIdAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`team_id expected to be basetypes.Int64Value, was: %T`, teamIdAttribute))
}
if diags.HasError() {
return nil, diags
}
return SecretsValue{
CreatedAt: createdAtVal,
Secret: secretVal,
TeamId: teamIdVal,
state: attr.ValueStateKnown,
}, diags
}
func NewSecretsValueNull() SecretsValue {
return SecretsValue{
state: attr.ValueStateNull,
}
}
func NewSecretsValueUnknown() SecretsValue {
return SecretsValue{
state: attr.ValueStateUnknown,
}
}
func NewSecretsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (SecretsValue, diag.Diagnostics) {
var diags diag.Diagnostics
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
ctx := context.Background()
for name, attributeType := range attributeTypes {
attribute, ok := attributes[name]
if !ok {
diags.AddError(
"Missing SecretsValue Attribute Value",
"While creating a SecretsValue value, a missing attribute value was detected. "+
"A SecretsValue must contain values for all attributes, even if null or unknown. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("SecretsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid SecretsValue Attribute Type",
"While creating a SecretsValue value, an invalid attribute value was detected. "+
"A SecretsValue must use a matching attribute type for the value. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("SecretsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("SecretsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra SecretsValue Attribute Value",
"While creating a SecretsValue value, an extra attribute value was detected. "+
"A SecretsValue must not contain values beyond the expected attribute types. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Extra SecretsValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewSecretsValueUnknown(), diags
}
createdAtAttribute, ok := attributes["created_at"]
if !ok {
diags.AddError(
"Attribute Missing",
`created_at is missing from object`)
return NewSecretsValueUnknown(), diags
}
createdAtVal, ok := createdAtAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`created_at expected to be basetypes.StringValue, was: %T`, createdAtAttribute))
}
secretAttribute, ok := attributes["secret"]
if !ok {
diags.AddError(
"Attribute Missing",
`secret is missing from object`)
return NewSecretsValueUnknown(), diags
}
secretVal, ok := secretAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`secret expected to be basetypes.StringValue, was: %T`, secretAttribute))
}
teamIdAttribute, ok := attributes["team_id"]
if !ok {
diags.AddError(
"Attribute Missing",
`team_id is missing from object`)
return NewSecretsValueUnknown(), diags
}
teamIdVal, ok := teamIdAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`team_id expected to be basetypes.Int64Value, was: %T`, teamIdAttribute))
}
if diags.HasError() {
return NewSecretsValueUnknown(), diags
}
return SecretsValue{
CreatedAt: createdAtVal,
Secret: secretVal,
TeamId: teamIdVal,
state: attr.ValueStateKnown,
}, diags
}
func NewSecretsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) SecretsValue {
object, diags := NewSecretsValue(attributeTypes, attributes)
if diags.HasError() {
// This could potentially be added to the diag package.
diagsStrings := make([]string, 0, len(diags))
for _, diagnostic := range diags {
diagsStrings = append(diagsStrings, fmt.Sprintf(
"%s | %s | %s",
diagnostic.Severity(),
diagnostic.Summary(),
diagnostic.Detail()))
}
panic("NewSecretsValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t SecretsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewSecretsValueNull(), nil
}
if !in.Type().Equal(t.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
return NewSecretsValueUnknown(), nil
}
if in.IsNull() {
return NewSecretsValueNull(), nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
return NewSecretsValueMust(SecretsValue{}.AttributeTypes(ctx), attributes), nil
}
func (t SecretsType) ValueType(ctx context.Context) attr.Value {
return SecretsValue{}
}
var _ basetypes.ObjectValuable = SecretsValue{}
type SecretsValue struct {
CreatedAt basetypes.StringValue `tfsdk:"created_at"`
Secret basetypes.StringValue `tfsdk:"secret"`
TeamId basetypes.Int64Value `tfsdk:"team_id"`
state attr.ValueState
}
func (v SecretsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 3)
var val tftypes.Value
var err error
attrTypes["created_at"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["secret"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["team_id"] = basetypes.Int64Type{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 3)
val, err = v.CreatedAt.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["created_at"] = val
val, err = v.Secret.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["secret"] = val
val, err = v.TeamId.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["team_id"] = val
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
case attr.ValueStateNull:
return tftypes.NewValue(objectType, nil), nil
case attr.ValueStateUnknown:
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
default:
panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
}
}
func (v SecretsValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v SecretsValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v SecretsValue) String() string {
return "SecretsValue"
}
func (v SecretsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
objVal, diags := types.ObjectValue(
map[string]attr.Type{
"created_at": basetypes.StringType{},
"secret": basetypes.StringType{},
"team_id": basetypes.Int64Type{},
},
map[string]attr.Value{
"created_at": v.CreatedAt,
"secret": v.Secret,
"team_id": v.TeamId,
})
return objVal, diags
}
func (v SecretsValue) Equal(o attr.Value) bool {
other, ok := o.(SecretsValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.CreatedAt.Equal(other.CreatedAt) {
return false
}
if !v.Secret.Equal(other.Secret) {
return false
}
if !v.TeamId.Equal(other.TeamId) {
return false
}
return true
}
func (v SecretsValue) Type(ctx context.Context) attr.Type {
return SecretsType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v SecretsValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"created_at": basetypes.StringType{},
"secret": basetypes.StringType{},
"team_id": basetypes.Int64Type{},
}
}

View file

@ -0,0 +1,299 @@
package provider
import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"strconv"
"terraform-provider-fleetdm/fleetdm_client"
)
// This file implements the "fleetdm_teams" Terraform resource.
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &teamsResource{}
_ resource.ResourceWithConfigure = &teamsResource{
client: nil,
}
_ resource.ResourceWithImportState = &teamsResource{}
)
// NewTeamsResource is a helper function to simplify the provider implementation.
func NewTeamsResource() resource.Resource {
return &teamsResource{
client: nil,
}
}
// teamsResource is the resource implementation.
type teamsResource struct {
client *fleetdm_client.FleetDMClient
}
// Configure adds the provider configured client to the resource.
func (r *teamsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*fleetdm_client.FleetDMClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *FleetDMClient, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
// Metadata returns the resource type name.
func (r *teamsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_teams"
}
// Schema defines the schema for the resource.
func (r *teamsResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = TeamResourceSchema(ctx)
}
// teamModelToTF is a handy function to take the results of the API call
// to the FleetDM API and convert it to the Terraform state.
func teamModelToTF(ctx context.Context, tm *fleetdm_client.TeamGetResponse, tf *TeamModel) error {
tf.Id = types.Int64Value(tm.Team.ID)
tf.Name = types.StringValue(tm.Team.Name)
tf.Description = types.StringValue(tm.Team.Description)
tf.Secrets = basetypes.NewListNull(NewSecretsValueNull().Type(ctx))
// Re-marshal agent_options into a string so TF can store it sanely.
aoBytes, err := json.Marshal(tm.Team.AgentOptions)
if err != nil {
return fmt.Errorf("failed to re-marshal agent options: %w", err)
}
tf.AgentOptions = types.StringValue(string(aoBytes))
return nil
}
// Create creates the resource from the plan and sets the initial Terraform state.
func (r *teamsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Retrieve values from plan
var plan TeamModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
newTeam, err := r.client.CreateTeam(plan.Name.ValueString(), plan.Description.ValueString())
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"Failed to create team",
fmt.Sprintf("Failed to create team: %s", err)))
return
}
if !plan.AgentOptions.IsNull() && !plan.AgentOptions.IsUnknown() {
aoPlan := plan.AgentOptions.ValueString()
if aoPlan != "" {
newTeam, err = r.client.UpdateAgentOptions(newTeam.Team.ID, aoPlan)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"failed to create agent options",
fmt.Sprintf("failed to save agent options: %s", err)))
// This is a problem. The interface terraform presents is that
// team creation with agent options is atomic, however under the
// hood it's two api calls. We need to clean up from the first
// call here, but this isn't atomic and it might fail.
err = r.client.DeleteTeam(newTeam.Team.ID)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"failed to clean up after failed team creation",
fmt.Sprintf("failed to delete team %s while cleaning up "+
"failure setting agent options: %s. Team will need to be "+
"manually deleted.", plan.Name.ValueString(), err)))
}
return
}
}
}
err = teamModelToTF(ctx, newTeam, &plan)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"failed to convert fleet api return to TF structs",
fmt.Sprintf("failed to convert fleet api return to TF structs: %s", err)))
_ = r.client.DeleteTeam(newTeam.Team.ID) // Problematic. :-/
return
}
// Set our state to match that of the plan, now that we have
// completed successfully
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read queries the FleetDM API and sets the TF state to what it finds.
func (r *teamsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state TeamModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
apiTeam, err := r.client.GetTeam(state.Id.ValueInt64())
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"Failed to get team",
fmt.Sprintf("Failed to get team: %s", err)))
return
}
if state.Id != types.Int64Value(apiTeam.Team.ID) {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"ID mismatch",
fmt.Sprintf("ID mismatch: %s != %s",
state.Id, strconv.FormatInt(apiTeam.Team.ID, 10))))
return
}
err = teamModelToTF(ctx, apiTeam, &state)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"failed to convert fleet api return to TF structs",
fmt.Sprintf("failed to convert fleet api return to TF structs: %s", err)))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Update will compare the plan and the current state and see how they
// differ. It will then update the team in FleetDM to match the plan,
// and then update the Terraform state to match that plan.
func (r *teamsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var state TeamModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var plan TeamModel
diags = req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var name, description *string
name = nil
description = nil
if !plan.Name.Equal(state.Name) {
n := plan.Name.ValueString()
name = &n
}
if !plan.Description.Equal(state.Description) {
d := plan.Description.ValueString()
description = &d
}
if name == nil && description == nil && plan.AgentOptions.Equal(state.AgentOptions) {
// Nothing to do
return
}
var upTeam *fleetdm_client.TeamGetResponse
var err error
// Deal with agent options first because it has a higher chance of failure
if !plan.AgentOptions.Equal(state.AgentOptions) {
ao := plan.AgentOptions.ValueString()
if ao != "" {
upTeam, err = r.client.UpdateAgentOptions(state.Id.ValueInt64(), ao)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"Failed to update agent options",
fmt.Sprintf("Failed to update agent options: %s", err)))
return
}
}
}
if name != nil || description != nil {
upTeam, err = r.client.UpdateTeam(state.Id.ValueInt64(), name, description)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"Failed to update team",
fmt.Sprintf("Failed to update team: %s", err)))
return
}
}
err = teamModelToTF(ctx, upTeam, &state)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"failed to convert fleet api return to TF structs",
fmt.Sprintf("failed to convert fleet api return to TF structs: %s", err)))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// ImportState enables import of teams. We accept the name of the team
// as input, query the FleetDM API to get the ID, and then set the ID.
// Terraform will turn around and call Read() on that id.
func (r *teamsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, err := r.client.TeamNameToID(req.ID)
if err != nil {
resp.Diagnostics.AddError("Failed to convert team name to ID",
fmt.Sprintf("Failed to convert team name to ID: %s", err))
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *teamsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state TeamModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteTeam(state.Id.ValueInt64())
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"Failed to delete team",
fmt.Sprintf("Failed to delete team: %s", err)))
return
}
resp.State.RemoveResource(ctx)
}

View file

@ -0,0 +1,222 @@
package provider
import (
"fmt"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
var defaultAgentOptions = `{"config":{"decorators":{"load":["SELECT uuid AS host_uuid FROM system_info;","SELECT hostname AS hostname FROM system_info;"]},"options":{"disable_distributed":false,"distributed_interval":10,"distributed_plugin":"tls","distributed_tls_max_attempts":3,"logger_tls_endpoint":"/api/osquery/log","logger_tls_period":10,"pack_delimiter":"/"}},"overrides":{}}`
var newAO = `{"command_line_flags":{"disable_events":true},"config":{"options":{"logger_tls_endpoint":"/test"}},"overrides":{}}`
var upAO = `{"command_line_flags":{"disable_events":true},"config":{"options":{"logger_tls_endpoint":"/update"}},"overrides":{}}`
// Notes:
// Set env var TF_ACC=1
// Set env var FLEETDM_APIKEY
// Set env var FLEETDM_URL (I couldn't figure out how to set this otherwise...)
// These tests use the Terraform acceptance testing framework to test
// our provider. It's a little bit magical, in that you define what
// the resource looks like in the Config and then write some Checks
// against what you actually get back. Implicit in each test is that
// it deletes the created resource at the end of the test.
func TestAccFleetdmTeams_basic(t *testing.T) {
teamName := fmt.Sprintf("aaa-%s",
acctest.RandStringFromCharSet(6, acctest.CharSetAlpha))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesome description"
}
`, teamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", teamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesome description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
},
})
}
func TestAccFleetdmTeams_agent_options(t *testing.T) {
teamName := fmt.Sprintf("aaa-%s",
acctest.RandStringFromCharSet(6, acctest.CharSetAlpha))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesome description"
agent_options = jsonencode(%s)
}
`, teamName, newAO),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", teamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesome description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", newAO),
),
},
},
})
}
func TestAccFleetdmTeams_update_all(t *testing.T) {
teamName := fmt.Sprintf("aaa-%s",
acctest.RandStringFromCharSet(6, acctest.CharSetAlpha))
NewTeamName := fmt.Sprintf("aaa-%s-new",
acctest.RandStringFromCharSet(6, acctest.CharSetAlpha))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesome description"
}
`, teamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", teamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesome description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesomer description"
agent_options = jsonencode(%s)
}
`, NewTeamName, upAO),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", NewTeamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesomer description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", upAO),
),
},
},
})
}
func TestAccFleetdmTeams_update_each(t *testing.T) {
teamName := fmt.Sprintf("aaa-%s",
acctest.RandStringFromCharSet(6, acctest.CharSetAlpha))
NewTeamName := fmt.Sprintf("aaa-%s-new",
acctest.RandStringFromCharSet(6, acctest.CharSetAlpha))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
}
`, teamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", teamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", ""),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesome description"
}
`, teamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", teamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesome description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesome description"
}
`, teamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", teamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesome description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesome description"
}
`, NewTeamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", NewTeamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesome description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesomer description"
}
`, NewTeamName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", NewTeamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesomer description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", defaultAgentOptions),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesomer description"
agent_options = jsonencode(%s)
}
`, NewTeamName, newAO),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", NewTeamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesomer description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", newAO),
),
},
{
Config: fmt.Sprintf(`
resource "fleetdm_teams" "test_team" {
name = "%s"
description = "Awesomer description"
agent_options = jsonencode(%s)
}
`, NewTeamName, upAO),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "name", NewTeamName),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "description", "Awesomer description"),
resource.TestCheckResourceAttr("fleetdm_teams.test_team", "agent_options", upAO),
),
},
},
})
}

View file

@ -0,0 +1,46 @@
terraform {
required_providers {
fleetdm = {
source = "fleetdm.com/tf/fleetdm"
}
}
}
provider "fleetdm" {
url = "https://something.cloud.fleetdm.com"
// apikey provided via FLEETDM_APIKEY
}
locals {
# Here are some default agent options.
default_agent_options = jsonencode(local.raw_agent_options)
raw_agent_options = {
"config" = {
"options" = {
pack_delimiter = "/"
logger_tls_period = 10
distributed_plugin = "tls"
disable_distributed = false
logger_tls_endpoint = "/api/osquery/log"
distributed_interval = 15
distributed_tls_max_attempts = 3
}
"decorators" = {
"load" = [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
}
}
command_line_flags = {
disable_events = true
enable_bpf_events = false
}
}
}
resource "fleetdm_teams" "hello_world" {
name = "my_first_team"
description = "This is my first team"
agent_options = local.default_agent_options
}