package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"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/marshallford/terraform-provider-ansible/pkg/ansible"
)
type ExecutionEnvironmentModel struct {
ContainerEngine types.String `tfsdk:"container_engine"`
Enabled types.Bool `tfsdk:"enabled"`
EnvironmentVariablesPass types.List `tfsdk:"environment_variables_pass"`
EnvironmentVariablesSet types.Map `tfsdk:"environment_variables_set"`
Image types.String `tfsdk:"image"`
PullArguments types.List `tfsdk:"pull_arguments"`
PullPolicy types.String `tfsdk:"pull_policy"`
ContainerOptions types.List `tfsdk:"container_options"`
}
type AnsibleOptionsModel struct {
ForceHandlers types.Bool `tfsdk:"force_handlers"`
SkipTags types.List `tfsdk:"skip_tags"`
StartAtTask types.String `tfsdk:"start_at_task"`
Limit types.List `tfsdk:"limit"`
Tags types.List `tfsdk:"tags"`
PrivateKeys types.List `tfsdk:"private_keys"`
KnownHosts types.List `tfsdk:"known_hosts"`
HostKeyChecking types.Bool `tfsdk:"host_key_checking"`
}
type PrivateKeyModel struct {
Name types.String `tfsdk:"name"`
Data types.String `tfsdk:"data"`
}
type ArtifactQueryModel struct {
JQFilter types.String `tfsdk:"jq_filter"`
Results types.List `tfsdk:"results"`
}
func navigatorRunDescriptions() map[string]attrDescription {
return map[string]attrDescription{
"playbook": {
Description: "Ansible playbook contents.",
MarkdownDescription: "Ansible [playbook](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html) contents.",
},
"inventory": {
Description: fmt.Sprintf("Ansible inventory contents. The environment variable '%s' is set to the path of the inventory in cases where '{{ inventory_file }}' cannot be referenced.", navigatorRunInventoryEnvVar),
MarkdownDescription: fmt.Sprintf("Ansible [inventory](https://docs.ansible.com/ansible/latest/getting_started/get_started_inventory.html) contents. The environment variable `%s` is set to the path of the inventory in cases where `{{ inventory_file }}` cannot be referenced.", navigatorRunInventoryEnvVar),
},
"working_directory": {
Description: fmt.Sprintf("Directory which '%s' is run from. Recommended to be the root Ansible content directory (sometimes called the project directory), which is likely to contain 'ansible.cfg', 'roles/', etc. Defaults to '%s'.", ansible.NavigatorProgram, defaultNavigatorRunWorkingDir),
MarkdownDescription: fmt.Sprintf("Directory which `%s` is run from. Recommended to be the root Ansible [content directory](https://docs.ansible.com/ansible/latest/tips_tricks/sample_setup.html#sample-directory-layout) (sometimes called the project directory), which is likely to contain `ansible.cfg`, `roles/`, etc. Defaults to `%s`.", ansible.NavigatorProgram, defaultNavigatorRunWorkingDir),
},
"execution_environment": {
Description: "Execution environment (EE) related configuration.",
MarkdownDescription: "[Execution environment](https://ansible.readthedocs.io/en/latest/getting_started_ee/index.html) (EE) related configuration.",
},
"ansible_navigator_binary": {
Description: fmt.Sprintf("Path to the '%s' binary. By default '$PATH' is searched.", ansible.NavigatorProgram),
MarkdownDescription: fmt.Sprintf("Path to the `%s` binary. By default `$PATH` is searched.", ansible.NavigatorProgram),
},
"ansible_options": {
Description: "Ansible playbook run related configuration.",
MarkdownDescription: "Ansible [playbook](https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html) run related configuration.",
},
"timezone": {
Description: fmt.Sprintf("IANA time zone, use 'local' for the system time zone. Defaults to '%s'.", defaultNavigatorRunTimezone),
MarkdownDescription: fmt.Sprintf("IANA time zone, use `local` for the system time zone. Defaults to `%s`.", defaultNavigatorRunTimezone),
},
"artifact_queries": {
Description: "Query the Ansible playbook artifact with 'jq' syntax. The playbook artifact contains detailed information about every play and task, as well as the stdout from the playbook run.",
MarkdownDescription: "Query the Ansible playbook artifact with [`jq`](https://jqlang.github.io/jq/) syntax. The [playbook artifact](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/2.0-ea/html/ansible_navigator_creator_guide/assembly-troubleshooting-navigator_ansible-navigator#proc-review-artifact_troubleshooting-navigator) contains detailed information about every play and task, as well as the stdout from the playbook run.",
},
"id": {
Description: "UUID.",
},
"command": {
Description: fmt.Sprintf("Generated '%s' run command. Useful for troubleshooting.", ansible.NavigatorProgram),
MarkdownDescription: fmt.Sprintf("Generated `%s` run command. Useful for troubleshooting.", ansible.NavigatorProgram),
},
}
}
func (ExecutionEnvironmentModel) descriptions() map[string]attrDescription {
return map[string]attrDescription{
"container_engine": {
Description: fmt.Sprintf("Container engine responsible for running the execution environment container image. Options: %s. Defaults to '%s'.", wrapElementsJoin(ansible.ContainerEngineOptions(true), "'"), defaultNavigatorRunContainerEngine),
MarkdownDescription: fmt.Sprintf("[Container engine](https://ansible.readthedocs.io/projects/navigator/settings/#container-engine) responsible for running the execution environment container image. Options: %s. Defaults to `%s`.", wrapElementsJoin(ansible.ContainerEngineOptions(true), "`"), defaultNavigatorRunContainerEngine),
},
"enabled": {
Description: fmt.Sprintf("Enable or disable the use of an execution environment. Disabling requires '%s' and is only recommended when without a container engine. Defaults to '%t'.", ansible.PlaybookProgram, defaultNavigatorRunEEEnabled),
MarkdownDescription: fmt.Sprintf("Enable or disable the use of an execution environment. Disabling requires `%s` and is only recommended when without a container engine. Defaults to `%t`.", ansible.PlaybookProgram, defaultNavigatorRunEEEnabled),
},
"environment_variables_pass": {
Description: "Existing environment variables to be passed through to and set within the execution environment.",
MarkdownDescription: "Existing environment variables to be [passed](https://ansible.readthedocs.io/projects/navigator/settings/#pass-environment-variable) through to and set within the execution environment.",
},
"environment_variables_set": {
Description: "Environment variables to be set within the execution environment.",
MarkdownDescription: "Environment variables to be [set](https://ansible.readthedocs.io/projects/navigator/settings/#set-environment-variable) within the execution environment.",
},
"image": {
Description: fmt.Sprintf("Name of the execution environment container image. Defaults to '%s'.", defaultNavigatorRunImage),
MarkdownDescription: fmt.Sprintf("Name of the execution environment container [image](https://ansible.readthedocs.io/projects/navigator/settings/#execution-environment-image). Defaults to `%s`.", defaultNavigatorRunImage),
},
"pull_arguments": {
Description: "Additional parameters that should be added to the pull command when pulling an execution environment container image from a container registry.",
MarkdownDescription: "Additional [parameters](https://ansible.readthedocs.io/projects/navigator/settings/#pull-arguments) that should be added to the pull command when pulling an execution environment container image from a container registry.",
},
"pull_policy": {
Description: fmt.Sprintf("Container image pull policy. Defaults to '%s'.", defaultNavigatorRunPullPolicy),
MarkdownDescription: fmt.Sprintf("Container image [pull policy](https://ansible.readthedocs.io/projects/navigator/settings/#pull-policy). Defaults to `%s`.", defaultNavigatorRunPullPolicy),
},
"container_options": {
Description: "Extra parameters passed to the container engine command.",
MarkdownDescription: "[Extra parameters](https://ansible.readthedocs.io/projects/navigator/settings/#container-options) passed to the container engine command.",
},
}
}
func (ExecutionEnvironmentModel) AttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"container_engine": types.StringType,
"enabled": types.BoolType,
"environment_variables_pass": types.ListType{ElemType: types.StringType},
"environment_variables_set": types.MapType{ElemType: types.StringType},
"image": types.StringType,
"pull_arguments": types.ListType{ElemType: types.StringType},
"pull_policy": types.StringType,
"container_options": types.ListType{ElemType: types.StringType},
}
}
func (ExecutionEnvironmentModel) Defaults() basetypes.ObjectValue {
return types.ObjectValueMust(
ExecutionEnvironmentModel{}.AttrTypes(),
map[string]attr.Value{
"container_engine": types.StringValue(defaultNavigatorRunContainerEngine),
"enabled": types.BoolValue(defaultNavigatorRunEEEnabled),
"environment_variables_pass": types.ListNull(types.StringType),
"environment_variables_set": types.MapNull(types.StringType),
"image": types.StringValue(defaultNavigatorRunImage),
"pull_arguments": types.ListNull(types.StringType),
"pull_policy": types.StringValue(defaultNavigatorRunPullPolicy),
"container_options": types.ListNull(types.StringType),
},
)
}
func (m ExecutionEnvironmentModel) Value(ctx context.Context, settings *ansible.NavigatorSettings) diag.Diagnostics {
var diags diag.Diagnostics
settings.ContainerEngine = m.ContainerEngine.ValueString()
settings.EEEnabled = m.Enabled.ValueBool()
var envVarsPass []string
if !m.EnvironmentVariablesPass.IsNull() {
diags.Append(m.EnvironmentVariablesPass.ElementsAs(ctx, &envVarsPass, false)...)
}
settings.EnvironmentVariablesPass = envVarsPass
envVarsSet := map[string]string{}
if !m.EnvironmentVariablesSet.IsNull() {
diags.Append(m.EnvironmentVariablesSet.ElementsAs(ctx, &envVarsSet, false)...)
}
settings.EnvironmentVariablesSet = envVarsSet
settings.Image = m.Image.ValueString()
var pullArguments []string
if !m.PullArguments.IsNull() {
diags.Append(m.PullArguments.ElementsAs(ctx, &pullArguments, false)...)
}
settings.PullArguments = pullArguments
settings.PullPolicy = m.PullPolicy.ValueString()
var containerOptions []string
if !m.ContainerOptions.IsNull() {
diags.Append(m.ContainerOptions.ElementsAs(ctx, &containerOptions, false)...)
}
settings.ContainerOptions = containerOptions
return diags
}
func (AnsibleOptionsModel) descriptions() map[string]attrDescription {
return map[string]attrDescription{
"force_handlers": {
Description: "Run handlers even if a task fails.",
},
"skip_tags": {
Description: "Only run plays and tasks whose tags do not match these values.",
},
"start_at_task": {
Description: "Start the playbook at the task matching this name.",
},
"limit": {
Description: "Further limit selected hosts to an additional pattern.",
},
"tags": {
Description: "Only run plays and tasks tagged with these values.",
},
"private_keys": {
Description: "SSH private keys used for authentication in addition to the automatically mounted default named keys and SSH agent socket path.",
MarkdownDescription: "SSH private keys used for authentication in addition to the [automatically mounted](https://ansible.readthedocs.io/projects/navigator/faq/#how-do-i-use-my-ssh-keys-with-an-execution-environment) default named keys and SSH agent socket path.",
},
"known_hosts": {
Description: fmt.Sprintf("SSH known host entries. Ansible variable '%s' set to path of 'known_hosts' file and SSH option 'UserKnownHostsFile' must be configured to said path. Defaults to all of the 'known_hosts' entries recorded.", ansible.SSHKnownHostsFileVar),
MarkdownDescription: fmt.Sprintf("SSH known host entries. Ansible variable `%s` set to path of `known_hosts` file and SSH option `UserKnownHostsFile` must be configured to said path. Defaults to all of the `known_hosts` entries recorded.", ansible.SSHKnownHostsFileVar),
},
"host_key_checking": {
Description: fmt.Sprintf("SSH host key checking. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible runner (library used by '%s') defaults this option to '%t' explicitly.", ansible.NavigatorProgram, ansible.RunnerDefaultHostKeyChecking),
MarkdownDescription: fmt.Sprintf("SSH host key checking. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible runner (library used by `%s`) defaults this option to `%t` explicitly.", ansible.NavigatorProgram, ansible.RunnerDefaultHostKeyChecking),
},
}
}
func (AnsibleOptionsModel) AttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"force_handlers": types.BoolType,
"skip_tags": types.ListType{ElemType: types.StringType},
"start_at_task": types.StringType,
"limit": types.ListType{ElemType: types.StringType},
"tags": types.ListType{ElemType: types.StringType},
"private_keys": types.ListType{ElemType: types.ObjectType{AttrTypes: PrivateKeyModel{}.AttrTypes()}},
"known_hosts": types.ListType{ElemType: types.StringType},
"host_key_checking": types.BoolType,
}
}
func (AnsibleOptionsModel) Defaults() basetypes.ObjectValue {
return types.ObjectValueMust(
AnsibleOptionsModel{}.AttrTypes(),
map[string]attr.Value{
"force_handlers": types.BoolNull(),
"skip_tags": types.ListNull(types.StringType),
"start_at_task": types.StringNull(),
"limit": types.ListNull(types.StringType),
"tags": types.ListNull(types.StringType),
"private_keys": types.ListNull(types.ObjectType{AttrTypes: PrivateKeyModel{}.AttrTypes()}),
"known_hosts": types.ListUnknown(types.StringType),
"host_key_checking": types.BoolNull(),
},
)
}
func (m AnsibleOptionsModel) Value(ctx context.Context, options *ansible.Options) diag.Diagnostics {
var diags diag.Diagnostics
options.Inventories = []string{navigatorRunName}
options.ForceHandlers = m.ForceHandlers.ValueBool()
var skipTags []string
if !m.SkipTags.IsNull() {
diags.Append(m.SkipTags.ElementsAs(ctx, &skipTags, false)...)
}
options.SkipTags = skipTags
options.StartAtTask = m.StartAtTask.ValueString()
var limit []string
if !m.Limit.IsNull() {
diags.Append(m.Limit.ElementsAs(ctx, &limit, false)...)
}
options.Limit = limit
var tags []string
if !m.Tags.IsNull() {
diags.Append(m.Tags.ElementsAs(ctx, &tags, false)...)
}
options.Tags = tags
var privateKeysModel []PrivateKeyModel
if !m.PrivateKeys.IsNull() {
diags.Append(m.PrivateKeys.ElementsAs(ctx, &privateKeysModel, false)...)
}
privateKeys := make([]string, 0, len(privateKeysModel))
for _, privateKeyModel := range privateKeysModel {
privateKeys = append(privateKeys, privateKeyModel.Name.ValueString())
}
options.PrivateKeys = privateKeys
options.KnownHosts = m.KnownHosts.IsUnknown() || len(m.KnownHosts.Elements()) > 0
options.HostKeyChecking = m.HostKeyChecking.ValueBool()
if m.HostKeyChecking.IsNull() {
options.HostKeyChecking = ansible.RunnerDefaultHostKeyChecking
}
return diags
}
func (m *AnsibleOptionsModel) Set(ctx context.Context, run navigatorRun) diag.Diagnostics {
var diags diag.Diagnostics
if m.KnownHosts.IsUnknown() {
knownHostsValue, newDiags := types.ListValueFrom(ctx, types.StringType, run.knownHosts)
diags.Append(newDiags...)
m.KnownHosts = knownHostsValue
}
return diags
}
func (PrivateKeyModel) descriptions() map[string]attrDescription {
return map[string]attrDescription{
"name": {
Description: "Key name.",
},
"data": {
Description: "Key data.",
},
}
}
func (PrivateKeyModel) AttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"name": types.StringType,
"data": types.StringType,
}
}
func (m PrivateKeyModel) Value(_ context.Context, key *ansible.PrivateKey) diag.Diagnostics {
var diags diag.Diagnostics
key.Name = m.Name.ValueString()
key.Data = m.Data.ValueString()
return diags
}
func (ArtifactQueryModel) descriptions() map[string]attrDescription {
return map[string]attrDescription{
"jq_filter": {
Description: "'jq' filter. Example: '.status, .stdout'.",
MarkdownDescription: "`jq` filter. Example: `.status, .stdout`.",
},
"results": {
Description: "Results of the 'jq' filter in JSON format.",
MarkdownDescription: "Results of the `jq` filter in JSON format.",
},
}
}
func (ArtifactQueryModel) AttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"jq_filter": types.StringType,
"results": types.ListType{ElemType: jsontypes.NormalizedType{}},
}
}
func (m ArtifactQueryModel) Value(_ context.Context, query *ansible.ArtifactQuery) diag.Diagnostics {
var diags diag.Diagnostics
query.JQFilter = m.JQFilter.ValueString()
query.Results = []string{} // m.Results always unknown when this function is called
return diags
}
func (m *ArtifactQueryModel) Set(ctx context.Context, query ansible.ArtifactQuery) diag.Diagnostics {
var diags diag.Diagnostics
m.JQFilter = types.StringValue(query.JQFilter)
resultsValue, newDiags := types.ListValueFrom(ctx, jsontypes.NormalizedType{}, query.Results)
diags.Append(newDiags...)
m.Results = resultsValue
return diags
}
//nolint:dupl
package provider
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ datasource.DataSource = &NavigatorRunDataSource{}
_ datasource.DataSourceWithConfigure = &NavigatorRunDataSource{}
)
func NewNavigatorRunDataSource() datasource.DataSource { //nolint:ireturn
return &NavigatorRunDataSource{}
}
type NavigatorRunDataSource struct {
opts *providerOptions
}
type NavigatorRunDataSourceModel struct {
Playbook types.String `tfsdk:"playbook"`
Inventory types.String `tfsdk:"inventory"`
WorkingDirectory types.String `tfsdk:"working_directory"`
ExecutionEnvironment types.Object `tfsdk:"execution_environment"`
AnsibleNavigatorBinary types.String `tfsdk:"ansible_navigator_binary"`
AnsibleOptions types.Object `tfsdk:"ansible_options"`
Timezone types.String `tfsdk:"timezone"`
ArtifactQueries types.Map `tfsdk:"artifact_queries"`
ID types.String `tfsdk:"id"`
Command types.String `tfsdk:"command"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
func (m NavigatorRunDataSourceModel) Value(ctx context.Context, run *navigatorRun, opts *providerOptions) diag.Diagnostics {
var diags diag.Diagnostics
run.dir = runDir(opts.BaseRunDirectory, m.ID.ValueString(), 0)
run.persistDir = opts.PersistRunDirectory
run.playbook = m.Playbook.ValueString()
run.inventories = []ansible.Inventory{{Name: navigatorRunName, Contents: m.Inventory.ValueString()}}
run.workingDir = m.WorkingDirectory.ValueString()
run.navigatorBinary = m.AnsibleNavigatorBinary.ValueString()
var eeModel ExecutionEnvironmentModel
diags.Append(m.ExecutionEnvironment.As(ctx, &eeModel, basetypes.ObjectAsOptions{})...)
run.navigatorSettings.Timezone = m.Timezone.ValueString()
diags.Append(eeModel.Value(ctx, &run.navigatorSettings)...)
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Value(ctx, &run.options)...)
var privateKeysModel []PrivateKeyModel
if !optsModel.PrivateKeys.IsNull() {
diags.Append(optsModel.PrivateKeys.ElementsAs(ctx, &privateKeysModel, false)...)
}
run.privateKeys = make([]ansible.PrivateKey, 0, len(privateKeysModel))
for _, model := range privateKeysModel {
var key ansible.PrivateKey
diags.Append(model.Value(ctx, &key)...)
run.privateKeys = append(run.privateKeys, key)
}
var knownHosts []string
if !optsModel.KnownHosts.IsUnknown() {
diags.Append(optsModel.KnownHosts.ElementsAs(ctx, &knownHosts, false)...)
}
run.knownHosts = knownHosts
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
run.artifactQueries = map[string]ansible.ArtifactQuery{}
for name, model := range queriesModel {
var query ansible.ArtifactQuery
diags.Append(model.Value(ctx, &query)...)
run.artifactQueries[name] = query
}
return diags
}
func (m *NavigatorRunDataSourceModel) Set(ctx context.Context, run navigatorRun) diag.Diagnostics {
var diags diag.Diagnostics
m.Command = types.StringValue(run.command)
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Set(ctx, run)...)
optsResults, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsModel)
diags.Append(newDiags...)
m.AnsibleOptions = optsResults
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
for name, model := range queriesModel {
diags.Append(model.Set(ctx, run.artifactQueries[name])...)
queriesModel[name] = model
}
queriesValue, newDiags := types.MapValueFrom(ctx, types.ObjectType{AttrTypes: ArtifactQueryModel{}.AttrTypes()}, queriesModel)
diags.Append(newDiags...)
m.ArtifactQueries = queriesValue
return diags
}
func (m *NavigatorRunDataSourceModel) SetDefaults(ctx context.Context) diag.Diagnostics {
var diags diag.Diagnostics
if m.WorkingDirectory.IsNull() {
m.WorkingDirectory = types.StringValue(defaultNavigatorRunWorkingDir)
}
if m.ExecutionEnvironment.IsNull() {
m.ExecutionEnvironment = ExecutionEnvironmentModel{}.Defaults()
}
var eeModel ExecutionEnvironmentModel
diags.Append(m.ExecutionEnvironment.As(ctx, &eeModel, basetypes.ObjectAsOptions{})...)
if eeModel.ContainerEngine.IsNull() {
eeModel.ContainerEngine = types.StringValue(defaultNavigatorRunContainerEngine)
}
if eeModel.Enabled.IsNull() {
eeModel.Enabled = types.BoolValue(defaultNavigatorRunEEEnabled)
}
if eeModel.Image.IsNull() {
eeModel.Image = types.StringValue(defaultNavigatorRunImage)
}
if eeModel.PullPolicy.IsNull() {
eeModel.PullPolicy = types.StringValue(defaultNavigatorRunPullPolicy)
}
eeValue, newDiags := types.ObjectValueFrom(ctx, ExecutionEnvironmentModel{}.AttrTypes(), eeModel)
diags.Append(newDiags...)
m.ExecutionEnvironment = eeValue
if m.AnsibleOptions.IsNull() {
m.AnsibleOptions = AnsibleOptionsModel{}.Defaults()
}
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
if optsModel.KnownHosts.IsNull() {
optsModel.KnownHosts = types.ListUnknown(types.StringType)
}
optsResults, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsModel)
diags.Append(newDiags...)
m.AnsibleOptions = optsResults
if m.Timezone.IsNull() {
m.Timezone = types.StringValue(defaultNavigatorRunTimezone)
}
return diags
}
func (d *NavigatorRunDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
//nolint:dupl
func (d *NavigatorRunDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: fmt.Sprintf("Run an Ansible playbook as a means to gather information. It is recommended to only run playbooks without observable side-effects. Requires '%s' and a container engine to run within an execution environment (EE).", ansible.NavigatorProgram),
MarkdownDescription: fmt.Sprintf("Run an Ansible playbook as a means to gather information. It is recommended to only run playbooks without observable side-effects. Requires `%s` and a container engine to run within an execution environment (EE).", ansible.NavigatorProgram),
Attributes: map[string]schema.Attribute{
// required
"playbook": schema.StringAttribute{
Description: navigatorRunDescriptions()["playbook"].Description,
MarkdownDescription: navigatorRunDescriptions()["playbook"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"inventory": schema.StringAttribute{
Description: navigatorRunDescriptions()["inventory"].Description,
MarkdownDescription: navigatorRunDescriptions()["inventory"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
// optional
"working_directory": schema.StringAttribute{
Description: navigatorRunDescriptions()["working_directory"].Description,
MarkdownDescription: navigatorRunDescriptions()["working_directory"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"execution_environment": schema.SingleNestedAttribute{
Description: navigatorRunDescriptions()["execution_environment"].Description,
MarkdownDescription: navigatorRunDescriptions()["execution_environment"].MarkdownDescription,
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"container_engine": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["container_engine"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["container_engine"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf(ansible.ContainerEngineOptions(true)...),
},
},
"enabled": schema.BoolAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["enabled"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["enabled"].MarkdownDescription,
Optional: true,
Computed: true,
},
"environment_variables_pass": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["environment_variables_pass"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["environment_variables_pass"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsEnvVarName()),
},
},
"environment_variables_set": schema.MapAttribute{
Description: fmt.Sprintf("%s '%s' is automatically set to '%s'.", ExecutionEnvironmentModel{}.descriptions()["environment_variables_set"].Description, navigatorRunOperationEnvVar, terraformOp(terraformOpRead)),
MarkdownDescription: fmt.Sprintf("%s `%s` is automatically set to `%s`.", ExecutionEnvironmentModel{}.descriptions()["environment_variables_set"].MarkdownDescription, navigatorRunOperationEnvVar, terraformOp(terraformOpRead)),
Optional: true,
ElementType: types.StringType,
Validators: []validator.Map{
mapvalidator.KeysAre(stringIsEnvVarName()),
},
},
"image": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["image"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["image"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringIsContainerImageName(),
},
},
"pull_arguments": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["pull_arguments"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["pull_arguments"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"pull_policy": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["pull_policy"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["pull_policy"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf(ansible.PullPolicyOptions()...),
},
},
"container_options": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["container_options"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["container_options"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
},
},
"ansible_navigator_binary": schema.StringAttribute{
Description: navigatorRunDescriptions()["ansible_navigator_binary"].Description,
MarkdownDescription: navigatorRunDescriptions()["ansible_navigator_binary"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"ansible_options": schema.SingleNestedAttribute{
Description: navigatorRunDescriptions()["ansible_options"].Description,
MarkdownDescription: navigatorRunDescriptions()["ansible_options"].MarkdownDescription,
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"force_handlers": schema.BoolAttribute{
Description: AnsibleOptionsModel{}.descriptions()["force_handlers"].Description,
Optional: true,
},
"skip_tags": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["skip_tags"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"start_at_task": schema.StringAttribute{
Description: AnsibleOptionsModel{}.descriptions()["start_at_task"].Description,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"limit": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["limit"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"tags": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["tags"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"private_keys": schema.ListNestedAttribute{
Description: AnsibleOptionsModel{}.descriptions()["private_keys"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["private_keys"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: PrivateKeyModel{}.descriptions()["name"].Description,
Required: true,
Validators: []validator.String{
stringIsSSHPrivateKeyName(),
},
},
"data": schema.StringAttribute{
Description: PrivateKeyModel{}.descriptions()["data"].Description,
Required: true,
Sensitive: true,
Validators: []validator.String{
stringIsSSHPrivateKey(),
},
},
},
},
},
"known_hosts": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["known_hosts"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["known_hosts"].MarkdownDescription,
Optional: true,
Computed: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsSSHKnownHost()),
},
},
"host_key_checking": schema.BoolAttribute{
Description: AnsibleOptionsModel{}.descriptions()["host_key_checking"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["host_key_checking"].MarkdownDescription,
Optional: true,
},
},
},
"timezone": schema.StringAttribute{
Description: navigatorRunDescriptions()["timezone"].Description,
MarkdownDescription: navigatorRunDescriptions()["timezone"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringIsIANATimezone(),
},
},
"artifact_queries": schema.MapNestedAttribute{
Description: navigatorRunDescriptions()["artifact_queries"].Description,
MarkdownDescription: navigatorRunDescriptions()["artifact_queries"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"jq_filter": schema.StringAttribute{
Description: ArtifactQueryModel{}.descriptions()["jq_filter"].Description,
MarkdownDescription: ArtifactQueryModel{}.descriptions()["jq_filter"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringIsJQFilter(),
},
},
"results": schema.ListAttribute{ // TODO switch to a dynamic attribute when supported as an element in a collection
Description: ArtifactQueryModel{}.descriptions()["results"].Description,
MarkdownDescription: ArtifactQueryModel{}.descriptions()["results"].MarkdownDescription,
Computed: true,
ElementType: jsontypes.NormalizedType{},
},
},
},
},
"id": schema.StringAttribute{
Description: navigatorRunDescriptions()["id"].Description,
Computed: true,
},
"command": schema.StringAttribute{
Description: navigatorRunDescriptions()["command"].Description,
MarkdownDescription: navigatorRunDescriptions()["command"].MarkdownDescription,
Computed: true,
},
// TODO include defaultNavigatorRunTimeout in description
"timeouts": timeouts.Attributes(ctx),
},
}
}
func (d *NavigatorRunDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
opts, ok := configureDataSourceClient(req, resp)
if !ok {
return
}
d.opts = opts
}
func (d *NavigatorRunDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data *NavigatorRunDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
resp.Diagnostics.Append(data.SetDefaults(ctx)...)
if resp.Diagnostics.HasError() {
return
}
timeout, newDiags := terraformOperationDataSourceTimeout(ctx, data.Timeouts, defaultNavigatorRunTimeout)
resp.Diagnostics.Append(newDiags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
data.ID = types.StringValue(uuid.New().String())
var navigatorRun navigatorRun
resp.Diagnostics.Append(data.Value(ctx, &navigatorRun, d.opts)...)
if resp.Diagnostics.HasError() {
return
}
run(ctx, &resp.Diagnostics, timeout, terraformOpRead, &navigatorRun)
resp.Diagnostics.Append(data.Set(ctx, navigatorRun)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
//nolint:dupl
package provider
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework-timeouts/ephemeral/timeouts"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ ephemeral.EphemeralResource = &NavigatorRunEphemeralResource{}
_ ephemeral.EphemeralResourceWithConfigure = &NavigatorRunEphemeralResource{}
)
func NewNavigatorRunEphemeralResource() ephemeral.EphemeralResource { //nolint:ireturn
return &NavigatorRunEphemeralResource{}
}
type NavigatorRunEphemeralResource struct {
opts *providerOptions
}
type NavigatorRunEphemeralResourceModel struct {
Playbook types.String `tfsdk:"playbook"`
Inventory types.String `tfsdk:"inventory"`
WorkingDirectory types.String `tfsdk:"working_directory"`
ExecutionEnvironment types.Object `tfsdk:"execution_environment"`
AnsibleNavigatorBinary types.String `tfsdk:"ansible_navigator_binary"`
AnsibleOptions types.Object `tfsdk:"ansible_options"`
Timezone types.String `tfsdk:"timezone"`
ArtifactQueries types.Map `tfsdk:"artifact_queries"`
ID types.String `tfsdk:"id"`
Command types.String `tfsdk:"command"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
func (m NavigatorRunEphemeralResourceModel) Value(ctx context.Context, run *navigatorRun, opts *providerOptions) diag.Diagnostics {
var diags diag.Diagnostics
run.dir = runDir(opts.BaseRunDirectory, m.ID.ValueString(), 0)
run.persistDir = opts.PersistRunDirectory
run.playbook = m.Playbook.ValueString()
run.inventories = []ansible.Inventory{{Name: navigatorRunName, Contents: m.Inventory.ValueString()}}
run.workingDir = m.WorkingDirectory.ValueString()
run.navigatorBinary = m.AnsibleNavigatorBinary.ValueString()
var eeModel ExecutionEnvironmentModel
diags.Append(m.ExecutionEnvironment.As(ctx, &eeModel, basetypes.ObjectAsOptions{})...)
run.navigatorSettings.Timezone = m.Timezone.ValueString()
diags.Append(eeModel.Value(ctx, &run.navigatorSettings)...)
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Value(ctx, &run.options)...)
var privateKeysModel []PrivateKeyModel
if !optsModel.PrivateKeys.IsNull() {
diags.Append(optsModel.PrivateKeys.ElementsAs(ctx, &privateKeysModel, false)...)
}
run.privateKeys = make([]ansible.PrivateKey, 0, len(privateKeysModel))
for _, model := range privateKeysModel {
var key ansible.PrivateKey
diags.Append(model.Value(ctx, &key)...)
run.privateKeys = append(run.privateKeys, key)
}
var knownHosts []string
if !optsModel.KnownHosts.IsUnknown() {
diags.Append(optsModel.KnownHosts.ElementsAs(ctx, &knownHosts, false)...)
}
run.knownHosts = knownHosts
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
run.artifactQueries = map[string]ansible.ArtifactQuery{}
for name, model := range queriesModel {
var query ansible.ArtifactQuery
diags.Append(model.Value(ctx, &query)...)
run.artifactQueries[name] = query
}
return diags
}
func (m *NavigatorRunEphemeralResourceModel) Set(ctx context.Context, run navigatorRun) diag.Diagnostics {
var diags diag.Diagnostics
m.Command = types.StringValue(run.command)
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Set(ctx, run)...)
optsResults, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsModel)
diags.Append(newDiags...)
m.AnsibleOptions = optsResults
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
for name, model := range queriesModel {
diags.Append(model.Set(ctx, run.artifactQueries[name])...)
queriesModel[name] = model
}
queriesValue, newDiags := types.MapValueFrom(ctx, types.ObjectType{AttrTypes: ArtifactQueryModel{}.AttrTypes()}, queriesModel)
diags.Append(newDiags...)
m.ArtifactQueries = queriesValue
return diags
}
func (m *NavigatorRunEphemeralResourceModel) SetDefaults(ctx context.Context) diag.Diagnostics {
var diags diag.Diagnostics
if m.WorkingDirectory.IsNull() {
m.WorkingDirectory = types.StringValue(defaultNavigatorRunWorkingDir)
}
if m.ExecutionEnvironment.IsNull() {
m.ExecutionEnvironment = ExecutionEnvironmentModel{}.Defaults()
}
var eeModel ExecutionEnvironmentModel
diags.Append(m.ExecutionEnvironment.As(ctx, &eeModel, basetypes.ObjectAsOptions{})...)
if eeModel.ContainerEngine.IsNull() {
eeModel.ContainerEngine = types.StringValue(defaultNavigatorRunContainerEngine)
}
if eeModel.Enabled.IsNull() {
eeModel.Enabled = types.BoolValue(defaultNavigatorRunEEEnabled)
}
if eeModel.Image.IsNull() {
eeModel.Image = types.StringValue(defaultNavigatorRunImage)
}
if eeModel.PullPolicy.IsNull() {
eeModel.PullPolicy = types.StringValue(defaultNavigatorRunPullPolicy)
}
eeValue, newDiags := types.ObjectValueFrom(ctx, ExecutionEnvironmentModel{}.AttrTypes(), eeModel)
diags.Append(newDiags...)
m.ExecutionEnvironment = eeValue
if m.AnsibleOptions.IsNull() {
m.AnsibleOptions = AnsibleOptionsModel{}.Defaults()
}
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
if optsModel.KnownHosts.IsNull() {
optsModel.KnownHosts = types.ListUnknown(types.StringType)
}
optsResults, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsModel)
diags.Append(newDiags...)
m.AnsibleOptions = optsResults
if m.Timezone.IsNull() {
m.Timezone = types.StringValue(defaultNavigatorRunTimezone)
}
return diags
}
func (er *NavigatorRunEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
//nolint:dupl
func (er *NavigatorRunEphemeralResource) Schema(ctx context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Description: fmt.Sprintf("Run an Ansible playbook as a means to gather temporary and likely sensitive information. It is recommended to only run playbooks without observable side-effects. Requires '%s' and a container engine to run within an execution environment (EE).", ansible.NavigatorProgram),
MarkdownDescription: fmt.Sprintf("Run an Ansible playbook as a means to gather temporary and likely sensitive information. It is recommended to only run playbooks without observable side-effects. Requires `%s` and a container engine to run within an execution environment (EE).", ansible.NavigatorProgram),
Attributes: map[string]schema.Attribute{
// required
"playbook": schema.StringAttribute{
Description: navigatorRunDescriptions()["playbook"].Description,
MarkdownDescription: navigatorRunDescriptions()["playbook"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"inventory": schema.StringAttribute{
Description: navigatorRunDescriptions()["inventory"].Description,
MarkdownDescription: navigatorRunDescriptions()["inventory"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
// optional
"working_directory": schema.StringAttribute{
Description: navigatorRunDescriptions()["working_directory"].Description,
MarkdownDescription: navigatorRunDescriptions()["working_directory"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"execution_environment": schema.SingleNestedAttribute{
Description: navigatorRunDescriptions()["execution_environment"].Description,
MarkdownDescription: navigatorRunDescriptions()["execution_environment"].MarkdownDescription,
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"container_engine": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["container_engine"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["container_engine"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf(ansible.ContainerEngineOptions(true)...),
},
},
"enabled": schema.BoolAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["enabled"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["enabled"].MarkdownDescription,
Optional: true,
Computed: true,
},
"environment_variables_pass": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["environment_variables_pass"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["environment_variables_pass"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsEnvVarName()),
},
},
"environment_variables_set": schema.MapAttribute{
Description: fmt.Sprintf("%s '%s' is automatically set to '%s'.", ExecutionEnvironmentModel{}.descriptions()["environment_variables_set"].Description, navigatorRunOperationEnvVar, terraformOp(terraformOpOpen)),
MarkdownDescription: fmt.Sprintf("%s `%s` is automatically set to `%s`.", ExecutionEnvironmentModel{}.descriptions()["environment_variables_set"].MarkdownDescription, navigatorRunOperationEnvVar, terraformOp(terraformOpOpen)),
Optional: true,
ElementType: types.StringType,
Validators: []validator.Map{
mapvalidator.KeysAre(stringIsEnvVarName()),
},
},
"image": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["image"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["image"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringIsContainerImageName(),
},
},
"pull_arguments": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["pull_arguments"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["pull_arguments"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"pull_policy": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["pull_policy"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["pull_policy"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf(ansible.PullPolicyOptions()...),
},
},
"container_options": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["container_options"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["container_options"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
},
},
"ansible_navigator_binary": schema.StringAttribute{
Description: navigatorRunDescriptions()["ansible_navigator_binary"].Description,
MarkdownDescription: navigatorRunDescriptions()["ansible_navigator_binary"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"ansible_options": schema.SingleNestedAttribute{
Description: navigatorRunDescriptions()["ansible_options"].Description,
MarkdownDescription: navigatorRunDescriptions()["ansible_options"].MarkdownDescription,
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"force_handlers": schema.BoolAttribute{
Description: AnsibleOptionsModel{}.descriptions()["force_handlers"].Description,
Optional: true,
},
"skip_tags": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["skip_tags"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"start_at_task": schema.StringAttribute{
Description: AnsibleOptionsModel{}.descriptions()["start_at_task"].Description,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"limit": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["limit"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"tags": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["tags"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"private_keys": schema.ListNestedAttribute{
Description: AnsibleOptionsModel{}.descriptions()["private_keys"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["private_keys"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: PrivateKeyModel{}.descriptions()["name"].Description,
Required: true,
Validators: []validator.String{
stringIsSSHPrivateKeyName(),
},
},
"data": schema.StringAttribute{
Description: PrivateKeyModel{}.descriptions()["data"].Description,
Required: true,
Sensitive: true,
Validators: []validator.String{
stringIsSSHPrivateKey(),
},
},
},
},
},
"known_hosts": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["known_hosts"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["known_hosts"].MarkdownDescription,
Optional: true,
Computed: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsSSHKnownHost()),
},
},
"host_key_checking": schema.BoolAttribute{
Description: AnsibleOptionsModel{}.descriptions()["host_key_checking"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["host_key_checking"].MarkdownDescription,
Optional: true,
},
},
},
"timezone": schema.StringAttribute{
Description: navigatorRunDescriptions()["timezone"].Description,
MarkdownDescription: navigatorRunDescriptions()["timezone"].MarkdownDescription,
Optional: true,
Computed: true,
Validators: []validator.String{
stringIsIANATimezone(),
},
},
"artifact_queries": schema.MapNestedAttribute{
Description: navigatorRunDescriptions()["artifact_queries"].Description,
MarkdownDescription: navigatorRunDescriptions()["artifact_queries"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"jq_filter": schema.StringAttribute{
Description: ArtifactQueryModel{}.descriptions()["jq_filter"].Description,
MarkdownDescription: ArtifactQueryModel{}.descriptions()["jq_filter"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringIsJQFilter(),
},
},
"results": schema.ListAttribute{ // TODO switch to a dynamic attribute when supported as an element in a collection
Description: ArtifactQueryModel{}.descriptions()["results"].Description,
MarkdownDescription: ArtifactQueryModel{}.descriptions()["results"].MarkdownDescription,
Computed: true,
ElementType: jsontypes.NormalizedType{},
},
},
},
},
"id": schema.StringAttribute{
Description: navigatorRunDescriptions()["id"].Description,
Computed: true,
},
"command": schema.StringAttribute{
Description: navigatorRunDescriptions()["command"].Description,
MarkdownDescription: navigatorRunDescriptions()["command"].MarkdownDescription,
Computed: true,
},
// TODO include defaultNavigatorRunTimeout in description
"timeouts": timeouts.Attributes(ctx),
},
}
}
func (er *NavigatorRunEphemeralResource) Configure(_ context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
opts, ok := configureEphemeralResourceClient(req, resp)
if !ok {
return
}
er.opts = opts
}
func (er *NavigatorRunEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var data *NavigatorRunEphemeralResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
resp.Diagnostics.Append(data.SetDefaults(ctx)...)
if resp.Diagnostics.HasError() {
return
}
timeout, newDiags := terraformOperationEphemeralResourceTimeout(ctx, data.Timeouts, defaultNavigatorRunTimeout)
resp.Diagnostics.Append(newDiags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
data.ID = types.StringValue(uuid.New().String())
var navigatorRun navigatorRun
resp.Diagnostics.Append(data.Value(ctx, &navigatorRun, er.opts)...)
if resp.Diagnostics.HasError() {
return
}
run(ctx, &resp.Diagnostics, timeout, terraformOpOpen, &navigatorRun)
resp.Diagnostics.Append(data.Set(ctx, navigatorRun)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}
package provider
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ resource.Resource = &NavigatorRunResource{}
_ resource.ResourceWithModifyPlan = &NavigatorRunResource{}
)
func NewNavigatorRunResource() resource.Resource { //nolint:ireturn
return &NavigatorRunResource{}
}
type NavigatorRunResource struct {
opts *providerOptions
}
type NavigatorRunResourceModel struct {
Playbook types.String `tfsdk:"playbook"`
Inventory types.String `tfsdk:"inventory"`
WorkingDirectory types.String `tfsdk:"working_directory"`
ExecutionEnvironment types.Object `tfsdk:"execution_environment"`
AnsibleNavigatorBinary types.String `tfsdk:"ansible_navigator_binary"`
AnsibleOptions types.Object `tfsdk:"ansible_options"`
Timezone types.String `tfsdk:"timezone"`
RunOnDestroy types.Bool `tfsdk:"run_on_destroy"`
DestroyPlaybook types.String `tfsdk:"destroy_playbook"`
Triggers types.Object `tfsdk:"triggers"`
ArtifactQueries types.Map `tfsdk:"artifact_queries"`
ID types.String `tfsdk:"id"`
Command types.String `tfsdk:"command"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
func (m NavigatorRunResourceModel) Value(ctx context.Context, run *navigatorRun, destroy bool, opts *providerOptions, runs uint32, previousInventory *string) diag.Diagnostics {
var diags diag.Diagnostics
run.dir = runDir(opts.BaseRunDirectory, m.ID.ValueString(), runs)
run.persistDir = opts.PersistRunDirectory
run.playbook = m.Playbook.ValueString()
if destroy && !m.DestroyPlaybook.IsNull() {
run.playbook = m.DestroyPlaybook.ValueString()
}
run.inventories = []ansible.Inventory{{Name: navigatorRunName, Contents: m.Inventory.ValueString()}}
if previousInventory != nil {
run.inventories = append(run.inventories, ansible.Inventory{Name: navigatorRunPrevInventoryName, Contents: *previousInventory, Exclude: true})
}
run.workingDir = m.WorkingDirectory.ValueString()
run.navigatorBinary = m.AnsibleNavigatorBinary.ValueString()
var eeModel ExecutionEnvironmentModel
diags.Append(m.ExecutionEnvironment.As(ctx, &eeModel, basetypes.ObjectAsOptions{})...)
run.navigatorSettings.Timezone = m.Timezone.ValueString()
diags.Append(eeModel.Value(ctx, &run.navigatorSettings)...)
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Value(ctx, &run.options)...)
var privateKeysModel []PrivateKeyModel
if !optsModel.PrivateKeys.IsNull() {
diags.Append(optsModel.PrivateKeys.ElementsAs(ctx, &privateKeysModel, false)...)
}
run.privateKeys = make([]ansible.PrivateKey, 0, len(privateKeysModel))
for _, model := range privateKeysModel {
var key ansible.PrivateKey
diags.Append(model.Value(ctx, &key)...)
run.privateKeys = append(run.privateKeys, key)
}
var knownHosts []string
if !optsModel.KnownHosts.IsUnknown() {
diags.Append(optsModel.KnownHosts.ElementsAs(ctx, &knownHosts, false)...)
}
run.knownHosts = knownHosts
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
run.artifactQueries = map[string]ansible.ArtifactQuery{}
for name, model := range queriesModel {
var query ansible.ArtifactQuery
diags.Append(model.Value(ctx, &query)...)
run.artifactQueries[name] = query
}
return diags
}
//nolint:dupl
func (m *NavigatorRunResourceModel) Set(ctx context.Context, run navigatorRun) diag.Diagnostics {
var diags diag.Diagnostics
m.Command = types.StringValue(run.command)
var optsModel AnsibleOptionsModel
diags.Append(m.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Set(ctx, run)...)
optsResults, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsModel)
diags.Append(newDiags...)
m.AnsibleOptions = optsResults
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
for name, model := range queriesModel {
diags.Append(model.Set(ctx, run.artifactQueries[name])...)
queriesModel[name] = model
}
queriesValue, newDiags := types.MapValueFrom(ctx, types.ObjectType{AttrTypes: ArtifactQueryModel{}.AttrTypes()}, queriesModel)
diags.Append(newDiags...)
m.ArtifactQueries = queriesValue
return diags
}
func (r *NavigatorRunResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
//nolint:maintidx,dupl
func (r *NavigatorRunResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: fmt.Sprintf("Run an Ansible playbook. Requires '%s' and a container engine to run within an execution environment (EE).", ansible.NavigatorProgram),
MarkdownDescription: fmt.Sprintf("Run an Ansible playbook. Requires `%s` and a container engine to run within an execution environment (EE).", ansible.NavigatorProgram),
Attributes: map[string]schema.Attribute{
// required
"playbook": schema.StringAttribute{
Description: navigatorRunDescriptions()["playbook"].Description,
MarkdownDescription: navigatorRunDescriptions()["playbook"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"inventory": schema.StringAttribute{
Description: fmt.Sprintf("%s In addition, the environment variable '%s' is set to the path of the last applied inventory when the resource is updated.", navigatorRunDescriptions()["inventory"].Description, navigatorRunPrevInventoryEnvVar),
MarkdownDescription: fmt.Sprintf("%s In addition, the environment variable `%s` is set to the path of the last applied inventory when the resource is updated.", navigatorRunDescriptions()["inventory"].MarkdownDescription, navigatorRunPrevInventoryEnvVar),
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
// optional
"working_directory": schema.StringAttribute{
Description: navigatorRunDescriptions()["working_directory"].Description,
MarkdownDescription: navigatorRunDescriptions()["working_directory"].MarkdownDescription,
Optional: true,
Computed: true,
Default: stringdefault.StaticString(defaultNavigatorRunWorkingDir),
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"execution_environment": schema.SingleNestedAttribute{
Description: navigatorRunDescriptions()["execution_environment"].Description,
MarkdownDescription: navigatorRunDescriptions()["execution_environment"].MarkdownDescription,
Optional: true,
Computed: true,
Default: objectdefault.StaticValue(ExecutionEnvironmentModel{}.Defaults()),
Attributes: map[string]schema.Attribute{
"container_engine": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["container_engine"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["container_engine"].MarkdownDescription,
Optional: true,
Computed: true,
Default: stringdefault.StaticString(defaultNavigatorRunContainerEngine),
Validators: []validator.String{
stringvalidator.OneOf(ansible.ContainerEngineOptions(true)...),
},
},
"enabled": schema.BoolAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["enabled"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["enabled"].MarkdownDescription,
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultNavigatorRunEEEnabled),
},
"environment_variables_pass": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["environment_variables_pass"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["environment_variables_pass"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsEnvVarName()),
},
},
"environment_variables_set": schema.MapAttribute{
Description: fmt.Sprintf("%s '%s' is automatically set to the current CRUD operation (%s).", ExecutionEnvironmentModel{}.descriptions()["environment_variables_set"].Description, navigatorRunOperationEnvVar, wrapElementsJoin(terraformOps([]terraformOp{terraformOpCreate, terraformOpUpdate, terraformOpDelete}).Strings(), "'")),
MarkdownDescription: fmt.Sprintf("%s `%s` is automatically set to the current CRUD operation (%s).", ExecutionEnvironmentModel{}.descriptions()["environment_variables_set"].MarkdownDescription, navigatorRunOperationEnvVar, wrapElementsJoin(terraformOps([]terraformOp{terraformOpCreate, terraformOpUpdate, terraformOpDelete}).Strings(), "`")),
Optional: true,
ElementType: types.StringType,
Validators: []validator.Map{
mapvalidator.KeysAre(stringIsEnvVarName()),
},
},
"image": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["image"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["image"].MarkdownDescription,
Optional: true,
Computed: true,
Default: stringdefault.StaticString(defaultNavigatorRunImage),
Validators: []validator.String{
stringIsContainerImageName(),
},
},
"pull_arguments": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["pull_arguments"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["pull_arguments"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"pull_policy": schema.StringAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["pull_policy"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["pull_policy"].MarkdownDescription,
Optional: true,
Computed: true,
Default: stringdefault.StaticString(defaultNavigatorRunPullPolicy),
Validators: []validator.String{
stringvalidator.OneOf(ansible.PullPolicyOptions()...),
},
},
"container_options": schema.ListAttribute{
Description: ExecutionEnvironmentModel{}.descriptions()["container_options"].Description,
MarkdownDescription: ExecutionEnvironmentModel{}.descriptions()["container_options"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
},
},
"ansible_navigator_binary": schema.StringAttribute{
Description: navigatorRunDescriptions()["ansible_navigator_binary"].Description,
MarkdownDescription: navigatorRunDescriptions()["ansible_navigator_binary"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"ansible_options": schema.SingleNestedAttribute{
Description: navigatorRunDescriptions()["ansible_options"].Description,
MarkdownDescription: navigatorRunDescriptions()["ansible_options"].MarkdownDescription,
Optional: true,
Computed: true,
Default: objectdefault.StaticValue(AnsibleOptionsModel{}.Defaults()),
Attributes: map[string]schema.Attribute{
"force_handlers": schema.BoolAttribute{
Description: AnsibleOptionsModel{}.descriptions()["force_handlers"].Description,
Optional: true,
},
"skip_tags": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["skip_tags"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"start_at_task": schema.StringAttribute{
Description: AnsibleOptionsModel{}.descriptions()["start_at_task"].Description,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"limit": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["limit"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"tags": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["tags"].Description,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"private_keys": schema.ListNestedAttribute{
Description: AnsibleOptionsModel{}.descriptions()["private_keys"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["private_keys"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: PrivateKeyModel{}.descriptions()["name"].Description,
Required: true,
Validators: []validator.String{
stringIsSSHPrivateKeyName(),
},
},
"data": schema.StringAttribute{
Description: PrivateKeyModel{}.descriptions()["data"].Description,
Required: true,
Sensitive: true,
Validators: []validator.String{
stringIsSSHPrivateKey(),
},
},
},
},
},
"known_hosts": schema.ListAttribute{
Description: AnsibleOptionsModel{}.descriptions()["known_hosts"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["known_hosts"].MarkdownDescription,
Optional: true,
Computed: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsSSHKnownHost()),
},
},
"host_key_checking": schema.BoolAttribute{
Description: AnsibleOptionsModel{}.descriptions()["host_key_checking"].Description,
MarkdownDescription: AnsibleOptionsModel{}.descriptions()["host_key_checking"].MarkdownDescription,
Optional: true,
},
},
},
"timezone": schema.StringAttribute{
Description: navigatorRunDescriptions()["timezone"].Description,
MarkdownDescription: navigatorRunDescriptions()["timezone"].MarkdownDescription,
Optional: true,
Computed: true,
Default: stringdefault.StaticString(defaultNavigatorRunTimezone),
Validators: []validator.String{
stringIsIANATimezone(),
},
},
"run_on_destroy": schema.BoolAttribute{
Description: fmt.Sprintf("Run playbook (or alternatively 'destroy_playbook' if configured) on destroy. The environment variable '%s' is set to '%s' during the run to allow for conditional plays, tasks, etc. Defaults to '%t'.", navigatorRunOperationEnvVar, terraformOp(terraformOpDelete), defaultNavigatorRunOnDestroy),
MarkdownDescription: fmt.Sprintf("Run playbook (or alternatively `destroy_playbook` if configured) on destroy. The environment variable `%s` is set to `%s` during the run to allow for conditional plays, tasks, etc. Defaults to `%t`.", navigatorRunOperationEnvVar, terraformOp(terraformOpDelete), defaultNavigatorRunOnDestroy),
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultNavigatorRunOnDestroy),
},
"destroy_playbook": schema.StringAttribute{
Description: fmt.Sprintf("%s Only run on destroy ('run_on_destroy' must be 'true').", navigatorRunDescriptions()["playbook"].Description),
MarkdownDescription: fmt.Sprintf("%s Only run on destroy (`run_on_destroy` must be `true`).", navigatorRunDescriptions()["playbook"].Description),
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"triggers": schema.SingleNestedAttribute{
Description: "Trigger various behaviors via arbitrary values.",
Optional: true,
Attributes: map[string]schema.Attribute{
"run": schema.DynamicAttribute{
Description: "A value that, when changed, will run the playbook again. Provides a way to initiate a run without changing other attributes such as the inventory or playbook.",
Optional: true,
},
"replace": schema.DynamicAttribute{
Description: "A value that, when changed, will recreate the resource. Serves as an alternative to the native 'replace_triggered_by' lifecycle argument. Will cause 'id' to change. May be useful when combined with 'run_on_destroy'.",
MarkdownDescription: "A value that, when changed, will recreate the resource. Serves as an alternative to the native [`replace_triggered_by`](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#replace_triggered_by) lifecycle argument. Will cause `id` to change. May be useful when combined with `run_on_destroy`.",
Optional: true,
PlanModifiers: []planmodifier.Dynamic{
dynamicplanmodifier.RequiresReplace(),
},
},
"known_hosts": schema.DynamicAttribute{
Description: "A value that, when changed, will reset the computed list of SSH known host entries. Useful when inventory hosts are recreated with the same hostnames/IP addresses, but different SSH keypairs.",
Optional: true,
},
},
},
"artifact_queries": schema.MapNestedAttribute{
Description: navigatorRunDescriptions()["artifact_queries"].Description,
MarkdownDescription: navigatorRunDescriptions()["artifact_queries"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"jq_filter": schema.StringAttribute{
Description: ArtifactQueryModel{}.descriptions()["jq_filter"].Description,
MarkdownDescription: ArtifactQueryModel{}.descriptions()["jq_filter"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringIsJQFilter(),
},
},
"results": schema.ListAttribute{ // TODO switch to a dynamic attribute when supported as an element in a collection
Description: ArtifactQueryModel{}.descriptions()["results"].Description,
MarkdownDescription: ArtifactQueryModel{}.descriptions()["results"].MarkdownDescription,
Computed: true,
ElementType: jsontypes.NormalizedType{},
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
},
},
},
"id": schema.StringAttribute{
Description: navigatorRunDescriptions()["id"].Description,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"command": schema.StringAttribute{
Description: navigatorRunDescriptions()["command"].Description,
MarkdownDescription: navigatorRunDescriptions()["command"].MarkdownDescription,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
// TODO include defaultNavigatorRunTimeout in description
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
Delete: true,
}),
},
}
}
// TODO find better solution.
func (NavigatorRunResource) TriggersAttr(data *NavigatorRunResourceModel, attribute string) attr.Value { //nolint:ireturn
if data.Triggers.IsNull() {
return types.DynamicNull()
}
return data.Triggers.Attributes()[attribute]
}
func (r *NavigatorRunResource) ShouldRun(plan *NavigatorRunResourceModel, state *NavigatorRunResourceModel) bool {
// skip working_directory, ansible_navigator_binary, run_on_destroy, timeouts
attributeChanges := []bool{
plan.Playbook.Equal(state.Playbook),
plan.Inventory.Equal(state.Inventory),
plan.ExecutionEnvironment.Equal(state.ExecutionEnvironment),
plan.AnsibleOptions.Equal(state.AnsibleOptions),
plan.Timezone.Equal(state.Timezone),
r.TriggersAttr(plan, "run").Equal(r.TriggersAttr(state, "run")),
plan.ArtifactQueries.Equal(state.ArtifactQueries),
}
for _, attributeChange := range attributeChanges {
if !attributeChange {
return true
}
}
return false
}
func (r *NavigatorRunResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
opts, ok := configureResourceClient(req, resp)
if !ok {
return
}
r.opts = opts
}
//nolint:cyclop
func (r *NavigatorRunResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
var data, state *NavigatorRunResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if req.Plan.Raw.IsNull() && state.RunOnDestroy.ValueBool() {
resp.Diagnostics.AddWarning(
"Resource Destruction Considerations",
"Applying this resource destruction with 'run_on_destroy' enabled will run the playbook as configured in state. "+
"The playbook run must complete successfully to remove the resource from Terraform state. ",
)
}
if req.State.Raw.IsNull() || req.Plan.Raw.IsNull() {
return
}
defer func() {
if !resp.Diagnostics.HasError() {
resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...)
}
}()
var optsPlanModel, optsStateModel AnsibleOptionsModel
resp.Diagnostics.Append(data.AnsibleOptions.As(ctx, &optsPlanModel, basetypes.ObjectAsOptions{})...)
resp.Diagnostics.Append(state.AnsibleOptions.As(ctx, &optsStateModel, basetypes.ObjectAsOptions{})...)
if optsPlanModel.KnownHosts.IsUnknown() && r.TriggersAttr(data, "known_hosts").Equal(r.TriggersAttr(state, "known_hosts")) {
optsPlanModel.KnownHosts = optsStateModel.KnownHosts
}
optsPlanValue, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsPlanModel)
resp.Diagnostics.Append(newDiags...)
data.AnsibleOptions = optsPlanValue
if !r.ShouldRun(data, state) {
return
}
data.Command = types.StringUnknown()
var artifactQueriesPlanModel map[string]ArtifactQueryModel
resp.Diagnostics.Append(data.ArtifactQueries.ElementsAs(ctx, &artifactQueriesPlanModel, false)...)
for name, model := range artifactQueriesPlanModel {
model.Results = types.ListUnknown(types.StringType)
artifactQueriesPlanModel[name] = model
}
artifactQueriesPlanValue, newDiags := types.MapValueFrom(ctx, types.ObjectType{AttrTypes: ArtifactQueryModel{}.AttrTypes()}, artifactQueriesPlanModel)
resp.Diagnostics.Append(newDiags...)
data.ArtifactQueries = artifactQueriesPlanValue
}
func (r *NavigatorRunResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *NavigatorRunResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
runs := uint32(1)
setRuns(ctx, &resp.Diagnostics, resp.Private.SetKey, runs)
if resp.Diagnostics.HasError() {
return
}
tflog.SetField(ctx, "runs", runs)
timeout, newDiags := terraformOperationResourceTimeout(ctx, terraformOpCreate, data.Timeouts, defaultNavigatorRunTimeout)
resp.Diagnostics.Append(newDiags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
data.ID = types.StringValue(uuid.New().String())
var navigatorRun navigatorRun
resp.Diagnostics.Append(data.Value(ctx, &navigatorRun, false, r.opts, runs, nil)...)
if resp.Diagnostics.HasError() {
return
}
run(ctx, &resp.Diagnostics, timeout, terraformOpCreate, &navigatorRun)
resp.Diagnostics.Append(data.Set(ctx, navigatorRun)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *NavigatorRunResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) {
}
func (r *NavigatorRunResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data, state *NavigatorRunResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
defer func() {
if !resp.Diagnostics.HasError() {
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
}()
if !r.ShouldRun(data, state) {
tflog.Debug(ctx, "skipping run")
return
}
runs := incrementRuns(ctx, &resp.Diagnostics, req.Private.GetKey, resp.Private.SetKey)
if resp.Diagnostics.HasError() {
return
}
tflog.SetField(ctx, "runs", runs)
timeout, newDiags := terraformOperationResourceTimeout(ctx, terraformOpUpdate, data.Timeouts, defaultNavigatorRunTimeout)
resp.Diagnostics.Append(newDiags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var navigatorRun navigatorRun
resp.Diagnostics.Append(data.Value(ctx, &navigatorRun, false, r.opts, runs, state.Inventory.ValueStringPointer())...)
if resp.Diagnostics.HasError() {
return
}
run(ctx, &resp.Diagnostics, timeout, terraformOpUpdate, &navigatorRun)
resp.Diagnostics.Append(data.Set(ctx, navigatorRun)...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *NavigatorRunResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *NavigatorRunResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if !data.RunOnDestroy.ValueBool() {
tflog.Debug(ctx, "skipping run, 'run_on_destroy' disabled")
return
}
runs := incrementRuns(ctx, &resp.Diagnostics, req.Private.GetKey, resp.Private.SetKey)
if resp.Diagnostics.HasError() {
return
}
tflog.SetField(ctx, "runs", runs)
timeout, newDiags := terraformOperationResourceTimeout(ctx, terraformOpDelete, data.Timeouts, defaultNavigatorRunTimeout)
resp.Diagnostics.Append(newDiags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var navigatorRun navigatorRun
resp.Diagnostics.Append(data.Value(ctx, &navigatorRun, true, r.opts, runs, nil)...)
if resp.Diagnostics.HasError() {
return
}
run(ctx, &resp.Diagnostics, timeout, terraformOpDelete, &navigatorRun)
}
package provider
import (
"context"
"fmt"
"os"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/path"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
const defaultProviderPersistRunDir = false
var _ provider.Provider = &AnsibleProvider{}
type AnsibleProvider struct {
version string
}
type AnsibleProviderModel struct {
BaseRunDirectory types.String `tfsdk:"base_run_directory"`
PersistRunDirectory types.Bool `tfsdk:"persist_run_directory"`
}
func (p *AnsibleProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "ansible"
resp.Version = p.version
}
func (p *AnsibleProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Run Ansible playbooks.",
MarkdownDescription: "Run [Ansible](https://github.com/ansible/ansible) playbooks.",
Attributes: map[string]schema.Attribute{
"base_run_directory": schema.StringAttribute{
Description: "Base directory in which to create run directories. On Unix systems this defaults to '$TMPDIR' if non-empty, else '/tmp'.",
MarkdownDescription: "Base directory in which to create run directories. On Unix systems this defaults to `$TMPDIR` if non-empty, else `/tmp`.",
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"persist_run_directory": schema.BoolAttribute{
Description: fmt.Sprintf("Remove run directory after the run completes. Useful when troubleshooting. Defaults to '%t'.", defaultProviderPersistRunDir),
MarkdownDescription: fmt.Sprintf("Remove run directory after the run completes. Useful when troubleshooting. Defaults to `%t`.", defaultProviderPersistRunDir),
Optional: true,
},
},
}
}
func (p *AnsibleProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var data AnsibleProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if data.BaseRunDirectory.IsUnknown() {
path := path.Root("base_run_directory")
summary, detail := unknownProviderValue(path)
resp.Diagnostics.AddAttributeError(path, summary, detail)
}
if data.PersistRunDirectory.IsUnknown() {
path := path.Root("persist_run_directory")
summary, detail := unknownProviderValue(path)
resp.Diagnostics.AddAttributeError(path, summary, detail)
}
if resp.Diagnostics.HasError() {
return
}
opts := providerOptions{
BaseRunDirectory: os.TempDir(),
PersistRunDirectory: defaultProviderPersistRunDir,
}
if !data.BaseRunDirectory.IsNull() {
opts.BaseRunDirectory = data.BaseRunDirectory.ValueString()
}
err := ansible.DirectoryPreflight(opts.BaseRunDirectory)
addPathError(&resp.Diagnostics, path.Root("base_run_directory"), "Base run directory preflight check", err)
if !data.PersistRunDirectory.IsNull() {
opts.PersistRunDirectory = data.PersistRunDirectory.ValueBool()
}
resp.ResourceData = &opts
resp.DataSourceData = &opts
resp.EphemeralResourceData = &opts
}
func (p *AnsibleProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewNavigatorRunResource,
}
}
func (p *AnsibleProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewNavigatorRunDataSource,
}
}
func (p *AnsibleProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
NewNavigatorRunEphemeralResource,
}
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &AnsibleProvider{
version: version,
}
}
}
package provider
import (
"context"
"fmt"
"strings"
"time"
dataSourceTimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts"
ephemeralResourceTimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/ephemeral/timeouts"
resourceTimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
const (
terraformOpCreate = iota
terraformOpRead = iota
terraformOpUpdate = iota
terraformOpDelete = iota
terraformOpOpen = iota
diagDetailPrefix = "Underlying error details"
)
type attrDescription struct {
Description string
MarkdownDescription string
}
type providerOptions struct {
BaseRunDirectory string
PersistRunDirectory bool
}
type (
terraformOp int
terraformOps []terraformOp
)
var terraformOpNames = []string{"create", "read", "update", "delete", "open"} //nolint:gochecknoglobals
func (op terraformOp) String() string {
return terraformOpNames[op]
}
func (ops terraformOps) Strings() []string {
output := make([]string, 0, len(ops))
for _, element := range ops {
output = append(output, element.String())
}
return output
}
func terraformOperationResourceTimeout(ctx context.Context, op terraformOp, value resourceTimeouts.Value, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) {
switch op {
case terraformOpCreate:
return value.Create(ctx, defaultTimeout)
case terraformOpRead:
return value.Read(ctx, defaultTimeout)
case terraformOpUpdate:
return value.Update(ctx, defaultTimeout)
case terraformOpDelete:
return value.Delete(ctx, defaultTimeout)
default:
return defaultTimeout, nil
}
}
func terraformOperationDataSourceTimeout(ctx context.Context, value dataSourceTimeouts.Value, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) {
return value.Read(ctx, defaultTimeout)
}
func terraformOperationEphemeralResourceTimeout(ctx context.Context, value ephemeralResourceTimeouts.Value, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) {
return value.Open(ctx, defaultTimeout)
}
func unknownProviderValue(value path.Path) (string, string) {
return fmt.Sprintf("Unknown configuration value '%s'", value),
fmt.Sprintf("The provider cannot be configured as there is an unknown configuration value for '%s'. ", value) +
"Either target apply the source of the value first or set the value statically in the configuration."
}
func unexpectedConfigureType(value string, providerData any) (string, string) {
return fmt.Sprintf("Unexpected %s Configure Type", value),
fmt.Sprintf("Expected *providerOptions, got: %T. Please report this issue to the provider developers.", providerData)
}
func configureResourceClient(req resource.ConfigureRequest, resp *resource.ConfigureResponse) (*providerOptions, bool) {
if req.ProviderData == nil {
return nil, false
}
opts, ok := req.ProviderData.(*providerOptions)
if !ok {
summary, detail := unexpectedConfigureType("Resource", req.ProviderData)
resp.Diagnostics.AddError(summary, detail)
}
return opts, ok
}
func configureDataSourceClient(req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) (*providerOptions, bool) {
if req.ProviderData == nil {
return nil, false
}
opts, ok := req.ProviderData.(*providerOptions)
if !ok {
summary, detail := unexpectedConfigureType("Data Source", req.ProviderData)
resp.Diagnostics.AddError(summary, detail)
}
return opts, ok
}
func configureEphemeralResourceClient(req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) (*providerOptions, bool) {
if req.ProviderData == nil {
return nil, false
}
opts, ok := req.ProviderData.(*providerOptions)
if !ok {
summary, detail := unexpectedConfigureType("Ephemeral Resource", req.ProviderData)
resp.Diagnostics.AddError(summary, detail)
}
return opts, ok
}
func addError(diags *diag.Diagnostics, summary string, err error) bool {
if err != nil {
diags.AddError(summary, fmt.Sprintf("%s: %s", diagDetailPrefix, err))
return true
}
return false
}
func addPathError(diags *diag.Diagnostics, path path.Path, summary string, err error) bool { //nolint:unparam
if err != nil {
diags.AddAttributeError(path, summary, fmt.Sprintf("%s: %s", diagDetailPrefix, err))
return true
}
return false
}
func addWarning(diags *diag.Diagnostics, summary string, err error) bool { //nolint:unparam
if err != nil {
diags.AddWarning(summary, fmt.Sprintf("%s: %s", diagDetailPrefix, err))
return true
}
return false
}
func wrapElements(input []string, wrap string) []string {
output := make([]string, 0, len(input))
for _, element := range input {
output = append(output, fmt.Sprintf("%s%s%s", wrap, element, wrap))
}
return output
}
func wrapElementsJoin(input []string, wrap string) string {
return strings.Join(wrapElements(input, wrap), ", ")
}
package provider
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"time"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
const (
navigatorRunName = "terraform"
navigatorRunPrevInventoryName = "previous-terraform"
navigatorRunDir = "tf-ansible-navigator-run"
navigatorRunOperationEnvVar = "ANSIBLE_TF_OPERATION"
navigatorRunInventoryEnvVar = "ANSIBLE_TF_INVENTORY"
navigatorRunPrevInventoryEnvVar = "ANSIBLE_TF_PREVIOUS_INVENTORY"
defaultNavigatorRunWorkingDir = "."
defaultNavigatorRunTimeout = 10 * time.Minute
defaultNavigatorRunContainerEngine = ansible.ContainerEngineAuto
defaultNavigatorRunEEEnabled = true
defaultNavigatorRunImage = "ghcr.io/ansible/community-ansible-dev-tools:v25.2.1"
defaultNavigatorRunPullPolicy = "tag"
defaultNavigatorRunTimezone = "UTC"
defaultNavigatorRunOnDestroy = false
)
type (
getKey func(ctx context.Context, key string) ([]byte, diag.Diagnostics)
setKey func(ctx context.Context, key string, value []byte) diag.Diagnostics
)
func setRuns(ctx context.Context, diags *diag.Diagnostics, setKey setKey, runs uint32) {
runsBytes, err := json.Marshal(runs)
if addError(diags, "Failed to set 'runs' private state", err) {
return
}
setKey(ctx, "runs", runsBytes)
}
func incrementRuns(ctx context.Context, diags *diag.Diagnostics, getKey getKey, setKey setKey) uint32 {
runsBytes, newDiags := getKey(ctx, "runs")
diags.Append(newDiags...)
runs := uint32(0)
if runsBytes != nil {
err := json.Unmarshal(runsBytes, &runs)
if addError(diags, "Failed to get 'runs' private state", err) {
return runs
}
}
runs++
runsBytes, err := json.Marshal(runs)
if addError(diags, "Failed to set 'runs' private state", err) {
return runs
}
setKey(ctx, "runs", runsBytes)
return runs
}
type navigatorRun struct {
dir string
persistDir bool
playbook string
inventories []ansible.Inventory
workingDir string
navigatorBinary string
options ansible.Options
navigatorSettings ansible.NavigatorSettings
privateKeys []ansible.PrivateKey
knownHosts []ansible.KnownHost
artifactQueries map[string]ansible.ArtifactQuery
command string
}
func run(ctx context.Context, diags *diag.Diagnostics, timeout time.Duration, operation terraformOp, run *navigatorRun) { //nolint:cyclop
var err error
ctx = tflog.SetField(ctx, "dir", run.dir)
ctx = tflog.SetField(ctx, "workingDir", run.workingDir)
tflog.Debug(ctx, "starting run")
tflog.Trace(ctx, "directory preflight")
err = ansible.DirectoryPreflight(run.workingDir)
addPathError(diags, path.Root("working_directory"), "Working directory preflight check", err)
if run.navigatorSettings.EEEnabled {
tflog.Trace(ctx, "container engine preflight")
err = ansible.ContainerEnginePreflight(run.navigatorSettings.ContainerEngine)
addPathError(diags, path.Root("execution_environment").AtMapKey("container_engine"), "Container engine preflight check", err)
} else {
tflog.Trace(ctx, "playbook preflight")
err = ansible.PlaybookPreflight()
addPathError(diags, path.Root("execution_environment").AtMapKey("enabled"), "Ansible playbook preflight check", err)
}
tflog.Trace(ctx, "navigator path preflight")
binary, err := ansible.NavigatorPathPreflight(run.navigatorBinary)
addPathError(diags, path.Root("ansible_navigator_binary"), "Ansible navigator not found", err)
tflog.Trace(ctx, "navigator preflight")
err = ansible.NavigatorPreflight(binary)
addPathError(diags, path.Root("ansible_navigator_binary"), "Ansible navigator preflight check", err)
tflog.Trace(ctx, "creating directories and files")
err = ansible.CreateRunDir(run.dir)
addError(diags, "Run directory not created", err)
err = ansible.CreatePlaybook(run.dir, run.playbook)
addError(diags, "Ansible playbook not created", err)
err = ansible.CreateInventories(run.dir, run.inventories, &run.navigatorSettings)
addError(diags, "Ansible inventories not created", err)
if len(run.privateKeys) > 0 {
err = ansible.CreatePrivateKeys(run.dir, run.privateKeys, &run.navigatorSettings)
addError(diags, "Private keys not created", err)
}
if run.options.KnownHosts {
err = ansible.CreateKnownHosts(run.dir, run.knownHosts, &run.navigatorSettings)
addError(diags, "Known hosts not created", err)
}
run.navigatorSettings.EnvironmentVariablesSet[navigatorRunOperationEnvVar] = operation.String()
run.navigatorSettings.EnvironmentVariablesSet[navigatorRunInventoryEnvVar] = ansible.InventoryPath(
run.dir,
navigatorRunName,
run.navigatorSettings.EEEnabled,
false,
)
if operation == terraformOpUpdate {
run.navigatorSettings.EnvironmentVariablesSet[navigatorRunPrevInventoryEnvVar] = ansible.InventoryPath(
run.dir,
navigatorRunPrevInventoryName,
run.navigatorSettings.EEEnabled,
true,
)
}
run.navigatorSettings.Timeout = timeout
navigatorSettingsContents, err := ansible.GenerateNavigatorSettings(&run.navigatorSettings)
addError(diags, "Ansible navigator settings not generated", err)
err = ansible.CreateNavigatorSettingsFile(run.dir, navigatorSettingsContents)
addError(diags, "Ansible navigator settings file not created", err)
if diags.HasError() {
if !run.persistDir {
err = ansible.RemoveRunDir(run.dir)
addWarning(diags, "Run directory not removed", err)
}
return
}
command := ansible.GenerateNavigatorRunCommand(
run.dir,
run.workingDir,
binary,
run.navigatorSettings.EEEnabled,
&run.options,
)
run.command = command.String()
commandOutput, err := ansible.ExecNavigatorRunCommand(command)
if err != nil {
output, _ := ansible.GetStdoutFromPlaybookArtifact(run.dir)
if output == "" {
output = commandOutput
}
status, _ := ansible.GetStatusFromPlaybookArtifact(run.dir)
switch status {
case "timeout":
addError(diags, "Ansible navigator run timed out", fmt.Errorf("%w\n\nOutput:\n%s", err, output))
default:
addError(diags, "Ansible navigator run failed", fmt.Errorf("%w\n\nOutput:\n%s", err, output))
}
}
if !diags.HasError() {
err = ansible.QueryPlaybookArtifact(run.dir, run.artifactQueries)
addPathError(diags, path.Root("artifact_queries"), "Playbook artifact queries failed", err)
if run.options.KnownHosts {
knownHosts, err := ansible.GetKnownHosts(run.dir)
addPathError(diags, path.Root("ansible_options").AtMapKey("known_hosts"), "Failed to get known hosts", err)
run.knownHosts = knownHosts
}
}
if !run.persistDir {
err = ansible.RemoveRunDir(run.dir)
addWarning(diags, "Run directory not removed", err)
}
}
func runDir(baseRunDirectory string, id string, runs uint32) string {
return filepath.Join(baseRunDirectory, fmt.Sprintf("%s-%s-%d", navigatorRunDir, id, runs))
}
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
type stringIsSSHPrivateKeyValidator struct{}
func (v stringIsSSHPrivateKeyValidator) Description(_ context.Context) string {
return "string must be an unencrypted SSH private key"
}
func (v stringIsSSHPrivateKeyValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsSSHPrivateKeyValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateSSHPrivateKey(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not an unencrypted SSH private key", err)
}
func stringIsSSHPrivateKey() stringIsSSHPrivateKeyValidator {
return stringIsSSHPrivateKeyValidator{}
}
type stringIsSSHPrivateKeyNameValidator struct{}
func (v stringIsSSHPrivateKeyNameValidator) Description(_ context.Context) string {
return "string must be a valid SSH private key name"
}
func (v stringIsSSHPrivateKeyNameValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsSSHPrivateKeyNameValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateSSHPrivateKeyName(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a valid SSH private key name", err)
}
func stringIsSSHPrivateKeyName() stringIsSSHPrivateKeyNameValidator {
return stringIsSSHPrivateKeyNameValidator{}
}
type stringIsSSHKnownHostValidator struct{}
func (v stringIsSSHKnownHostValidator) Description(_ context.Context) string {
return "string must be a SSH known host entry"
}
func (v stringIsSSHKnownHostValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsSSHKnownHostValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateSSHKnownHost(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a single SSH known host entry", err)
}
func stringIsSSHKnownHost() stringIsSSHKnownHostValidator {
return stringIsSSHKnownHostValidator{}
}
type stringIsEnvVarNameValidator struct{}
func (v stringIsEnvVarNameValidator) Description(_ context.Context) string {
return "string must be an environment variable name"
}
func (v stringIsEnvVarNameValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsEnvVarNameValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateEnvVarName(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a valid environment variable name", err)
}
func stringIsEnvVarName() stringIsEnvVarNameValidator {
return stringIsEnvVarNameValidator{}
}
type stringIsYAMLValidator struct{}
func (v stringIsYAMLValidator) Description(_ context.Context) string {
return "string must be YAML"
}
func (v stringIsYAMLValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsYAMLValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateYAML(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not valid YAML", err)
}
func stringIsYAML() stringIsYAMLValidator {
return stringIsYAMLValidator{}
}
type stringIsIANATimezoneValidator struct{}
func (v stringIsIANATimezoneValidator) Description(_ context.Context) string {
return "string must be an IANA time zone"
}
func (v stringIsIANATimezoneValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsIANATimezoneValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateIANATimezone(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a valid IANA time zone, use 'local' for the system time zone", err)
}
func stringIsIANATimezone() stringIsIANATimezoneValidator {
return stringIsIANATimezoneValidator{}
}
type stringIsJQFilterValidator struct{}
func (v stringIsJQFilterValidator) Description(_ context.Context) string {
return "string must be a JQ filter"
}
func (v stringIsJQFilterValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsJQFilterValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateJQFilter(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a valid JQ filter", err)
}
func stringIsJQFilter() stringIsJQFilterValidator {
return stringIsJQFilterValidator{}
}
type stringIsContainerImageNameValidator struct{}
func (v stringIsContainerImageNameValidator) Description(_ context.Context) string {
return "string must be a container image name"
}
func (v stringIsContainerImageNameValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v stringIsContainerImageNameValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() {
return
}
err := ansible.ValidateContainerImageName(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a valid container image name", err)
}
func stringIsContainerImageName() stringIsContainerImageNameValidator {
return stringIsContainerImageNameValidator{}
}