package provider
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-timeouts/action/timeouts"
"github.com/hashicorp/terraform-plugin-framework/action"
"github.com/hashicorp/terraform-plugin-framework/action/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ action.Action = (*NavigatorRunAction)(nil)
_ action.ActionWithConfigure = (*NavigatorRunAction)(nil)
)
type NavigatorRunActionModel struct {
NavigatorRunCommonModel
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
func (m NavigatorRunActionModel) Value(ctx context.Context, opts *providerOptions, runData *navigatorRunData) diag.Diagnostics {
var diags diag.Diagnostics
*runData = navigatorRunData{
hostDir: navigatorRunDirPath(opts.BaseRunDirectory, uuid.New().String(), 0),
persistDir: opts.PersistRunDirectory,
}
diags.Append(runData.Load(ctx, m.NavigatorRunCommonModel)...)
return diags
}
type NavigatorRunAction struct {
opts *providerOptions
}
func NewNavigatorRunAction() action.Action { //nolint:ireturn
return &NavigatorRunAction{}
}
func (a *NavigatorRunAction) Metadata(_ context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
func (a *NavigatorRunAction) Schema(ctx context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) {
description := navigatorRunDescription(surfaceAction)
attributes := actionAttributes(navigatorRunAttributes(surfaceAction))
// TODO include defaultNavigatorRunTimeout in description
attributes["timeouts"] = timeouts.Attributes(ctx)
resp.Schema = schema.Schema{
Description: description.Description,
MarkdownDescription: description.MarkdownDescription,
Attributes: attributes,
}
}
func (a *NavigatorRunAction) Configure(_ context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) {
opts, ok := configureActionClient(req, resp)
if !ok {
return
}
a.opts = opts
}
func (a *NavigatorRunAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
var data *NavigatorRunActionModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
resp.Diagnostics.Append(data.SetDefaults(ctx)...)
if resp.Diagnostics.HasError() {
return
}
timeout, newDiags := terraformOperationActionTimeout(ctx, data.Timeouts, defaultNavigatorRunTimeout)
resp.Diagnostics.Append(newDiags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout+navigatorRunTimeoutOverhead)
defer cancel()
var runData navigatorRunData
resp.Diagnostics.Append(data.Value(ctx, a.opts, &runData)...)
if resp.Diagnostics.HasError() {
return
}
runData.playbookArtifactQueries = map[string]ansible.PlaybookArtifactQuery{
"stdout": {JQFilter: ".stdout[]", Raw: true},
}
runData.operation = terraformOpInvoke
runData.config.Settings.Timeout = timeout
run(ctx, &resp.Diagnostics, &runData)
if resp.Diagnostics.HasError() {
return
}
stdout := strings.Join(runData.playbookArtifactQueries["stdout"].Results, "\n")
resp.SendProgress(action.InvokeProgressEvent{Message: stdout})
}
package provider
import (
"context"
"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"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible/navigator"
)
type NavigatorRunCommonModel 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"`
}
func (m *NavigatorRunCommonModel) 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
}
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 {
ExtraVars types.String `tfsdk:"extra_vars"`
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 (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() types.Object {
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, execEnv *navigator.ExecutionEnvironment) diag.Diagnostics {
var diags diag.Diagnostics
execEnv.ContainerEngine = navigator.ContainerEngine(m.ContainerEngine.ValueString())
execEnv.Enabled = m.Enabled.ValueBool()
var envVarsPass []string
if !m.EnvironmentVariablesPass.IsNull() {
diags.Append(m.EnvironmentVariablesPass.ElementsAs(ctx, &envVarsPass, false)...)
}
execEnv.EnvironmentVariables.Pass = envVarsPass
envVarsSet := map[string]string{}
if !m.EnvironmentVariablesSet.IsNull() {
diags.Append(m.EnvironmentVariablesSet.ElementsAs(ctx, &envVarsSet, false)...)
}
execEnv.EnvironmentVariables.Set = envVarsSet
execEnv.Image = m.Image.ValueString()
var pullArguments []string
if !m.PullArguments.IsNull() {
diags.Append(m.PullArguments.ElementsAs(ctx, &pullArguments, false)...)
}
execEnv.Pull.Arguments = pullArguments
execEnv.Pull.Policy = navigator.PullPolicy(m.PullPolicy.ValueString())
var containerOptions []string
if !m.ContainerOptions.IsNull() {
diags.Append(m.ContainerOptions.ElementsAs(ctx, &containerOptions, false)...)
}
execEnv.ContainerOptions = containerOptions
return diags
}
func (AnsibleOptionsModel) AttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"extra_vars": types.StringType,
"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() types.Object {
return types.ObjectValueMust(
AnsibleOptionsModel{}.AttrTypes(),
map[string]attr.Value{
"extra_vars": types.StringNull(),
"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.PlaybookOptions) diag.Diagnostics {
var diags diag.Diagnostics
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
return diags
}
func (m *AnsibleOptionsModel) Set(ctx context.Context, run navigatorRunData) 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) 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) 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.PlaybookArtifactQuery) 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.PlaybookArtifactQuery) 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 // surface plumbing, not schema, is what still overlaps
package provider
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts"
"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/types"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ datasource.DataSource = (*NavigatorRunDataSource)(nil)
_ datasource.DataSourceWithConfigure = (*NavigatorRunDataSource)(nil)
)
type NavigatorRunDataSourceModel struct {
NavigatorRunCommonModel
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, opts *providerOptions, runData *navigatorRunData) diag.Diagnostics {
var diags diag.Diagnostics
*runData = navigatorRunData{
hostDir: navigatorRunDirPath(opts.BaseRunDirectory, m.ID.ValueString(), 0),
persistDir: opts.PersistRunDirectory,
}
diags.Append(runData.Load(ctx, m.NavigatorRunCommonModel)...)
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
runData.userArtifactQueries = true
runData.playbookArtifactQueries = map[string]ansible.PlaybookArtifactQuery{}
for name, model := range queriesModel {
var query ansible.PlaybookArtifactQuery
diags.Append(model.Value(ctx, &query)...)
runData.playbookArtifactQueries[name] = query
}
return diags
}
func (m *NavigatorRunDataSourceModel) Set(ctx context.Context, run navigatorRunData) diag.Diagnostics {
return run.Store(ctx, &m.Command, &m.AnsibleOptions, &m.ArtifactQueries)
}
type NavigatorRunDataSource struct {
opts *providerOptions
}
func NewNavigatorRunDataSource() datasource.DataSource { //nolint:ireturn
return &NavigatorRunDataSource{}
}
func (d *NavigatorRunDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
func (d *NavigatorRunDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := navigatorRunDescription(surfaceDataSource)
attributes := dataSourceAttributes(navigatorRunAttributes(surfaceDataSource))
// TODO include defaultNavigatorRunTimeout in description
attributes["timeouts"] = timeouts.Attributes(ctx)
resp.Schema = schema.Schema{
Description: description.Description,
MarkdownDescription: description.MarkdownDescription,
Attributes: attributes,
}
}
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+navigatorRunTimeoutOverhead)
defer cancel()
data.ID = types.StringValue(uuid.New().String())
var runData navigatorRunData
resp.Diagnostics.Append(data.Value(ctx, d.opts, &runData)...)
if resp.Diagnostics.HasError() {
return
}
runData.operation = terraformOpRead
runData.config.Settings.Timeout = timeout
run(ctx, &resp.Diagnostics, &runData)
resp.Diagnostics.Append(data.Set(ctx, runData)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
//nolint:dupl // surface plumbing, not schema, is what still overlaps
package provider
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-timeouts/ephemeral/timeouts"
"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/types"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ ephemeral.EphemeralResource = (*NavigatorRunEphemeralResource)(nil)
_ ephemeral.EphemeralResourceWithConfigure = (*NavigatorRunEphemeralResource)(nil)
)
type NavigatorRunEphemeralResourceModel struct {
NavigatorRunCommonModel
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, opts *providerOptions, runData *navigatorRunData) diag.Diagnostics {
var diags diag.Diagnostics
*runData = navigatorRunData{
hostDir: navigatorRunDirPath(opts.BaseRunDirectory, m.ID.ValueString(), 0),
persistDir: opts.PersistRunDirectory,
}
diags.Append(runData.Load(ctx, m.NavigatorRunCommonModel)...)
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
runData.userArtifactQueries = true
runData.playbookArtifactQueries = map[string]ansible.PlaybookArtifactQuery{}
for name, model := range queriesModel {
var query ansible.PlaybookArtifactQuery
diags.Append(model.Value(ctx, &query)...)
runData.playbookArtifactQueries[name] = query
}
return diags
}
func (m *NavigatorRunEphemeralResourceModel) Set(ctx context.Context, run navigatorRunData) diag.Diagnostics {
return run.Store(ctx, &m.Command, &m.AnsibleOptions, &m.ArtifactQueries)
}
type NavigatorRunEphemeralResource struct {
opts *providerOptions
}
func NewNavigatorRunEphemeralResource() ephemeral.EphemeralResource { //nolint:ireturn
return &NavigatorRunEphemeralResource{}
}
func (er *NavigatorRunEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
func (er *NavigatorRunEphemeralResource) Schema(ctx context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
description := navigatorRunDescription(surfaceEphemeral)
attributes := ephemeralAttributes(navigatorRunAttributes(surfaceEphemeral))
// TODO include defaultNavigatorRunTimeout in description
attributes["timeouts"] = timeouts.Attributes(ctx)
resp.Schema = schema.Schema{
Description: description.Description,
MarkdownDescription: description.MarkdownDescription,
Attributes: attributes,
}
}
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+navigatorRunTimeoutOverhead)
defer cancel()
data.ID = types.StringValue(uuid.New().String())
var runData navigatorRunData
resp.Diagnostics.Append(data.Value(ctx, er.opts, &runData)...)
if resp.Diagnostics.HasError() {
return
}
runData.operation = terraformOpOpen
runData.config.Settings.Timeout = timeout
run(ctx, &resp.Diagnostics, &runData)
resp.Diagnostics.Append(data.Set(ctx, runData)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}
package provider
import (
"context"
"fmt"
"slices"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"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/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)(nil)
_ resource.ResourceWithConfigure = (*NavigatorRunResource)(nil)
_ resource.ResourceWithModifyPlan = (*NavigatorRunResource)(nil)
)
type NavigatorRunResourceModel struct {
NavigatorRunCommonModel
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, destroy bool, opts *providerOptions, runs uint32, previousInventory *string, runData *navigatorRunData) diag.Diagnostics {
var diags diag.Diagnostics
*runData = navigatorRunData{
hostDir: navigatorRunDirPath(opts.BaseRunDirectory, m.ID.ValueString(), runs),
persistDir: opts.PersistRunDirectory,
}
diags.Append(runData.Load(ctx, m.NavigatorRunCommonModel)...)
if destroy && !m.DestroyPlaybook.IsNull() {
runData.config.Playbook = m.DestroyPlaybook.ValueString()
}
if previousInventory != nil {
runData.config.Inventories = append(runData.config.Inventories, ansible.Inventory{Name: navigatorRunPrevInventoryName, Contents: *previousInventory, Exclude: true})
}
var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)
runData.userArtifactQueries = true
runData.playbookArtifactQueries = map[string]ansible.PlaybookArtifactQuery{}
for name, model := range queriesModel {
var query ansible.PlaybookArtifactQuery
diags.Append(model.Value(ctx, &query)...)
runData.playbookArtifactQueries[name] = query
}
return diags
}
func (m *NavigatorRunResourceModel) Set(ctx context.Context, run navigatorRunData) diag.Diagnostics {
return run.Store(ctx, &m.Command, &m.AnsibleOptions, &m.ArtifactQueries)
}
func (m *NavigatorRunResourceModel) Trigger(name string) attr.Value { //nolint:ireturn
if m.Triggers.IsNull() {
return types.DynamicNull()
}
return m.Triggers.Attributes()[name]
}
func (m *NavigatorRunResourceModel) ShouldRun(state *NavigatorRunResourceModel) bool {
if !m.Trigger("exclusive_run").IsNull() {
return !m.Trigger("exclusive_run").Equal(state.Trigger("exclusive_run"))
}
// skip working_directory, ansible_navigator_binary, run_on_destroy, destroy_playbook, timeouts
unchanged := []bool{
m.Playbook.Equal(state.Playbook),
m.Inventory.Equal(state.Inventory),
m.ExecutionEnvironment.Equal(state.ExecutionEnvironment),
m.AnsibleOptions.Equal(state.AnsibleOptions),
m.Timezone.Equal(state.Timezone),
m.Trigger("run").Equal(state.Trigger("run")),
m.ArtifactQueries.Equal(state.ArtifactQueries),
}
return slices.Contains(unchanged, false)
}
type NavigatorRunResource struct {
opts *providerOptions
}
func NewNavigatorRunResource() resource.Resource { //nolint:ireturn
return &NavigatorRunResource{}
}
func (r *NavigatorRunResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_navigator_run", req.ProviderTypeName)
}
func (r *NavigatorRunResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := navigatorRunDescription(surfaceResource)
attributes := navigatorRunAttributes(surfaceResource)
// TODO include defaultNavigatorRunTimeout in description
attributes["timeouts"] = timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
Delete: true,
})
resp.Schema = schema.Schema{
Description: description.Description,
MarkdownDescription: description.MarkdownDescription,
Attributes: attributes,
}
}
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() && data.Trigger("known_hosts").Equal(state.Trigger("known_hosts")) {
tflog.Trace(ctx, "keeping known hosts from state")
optsPlanModel.KnownHosts = optsStateModel.KnownHosts
}
optsPlanValue, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsPlanModel)
resp.Diagnostics.Append(newDiags...)
data.AnsibleOptions = optsPlanValue
if !data.ShouldRun(state) {
tflog.Debug(ctx, "planning no run", map[string]any{"reason": "no changes to run for"})
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
}
ctx = 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+navigatorRunTimeoutOverhead)
defer cancel()
data.ID = types.StringValue(uuid.New().String())
var runData navigatorRunData
resp.Diagnostics.Append(data.Value(ctx, false, r.opts, runs, nil, &runData)...)
if resp.Diagnostics.HasError() {
return
}
runData.operation = terraformOpCreate
runData.config.Settings.Timeout = timeout
run(ctx, &resp.Diagnostics, &runData)
resp.Diagnostics.Append(data.Set(ctx, runData)...)
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 !data.ShouldRun(state) {
tflog.Debug(ctx, "skipping run", map[string]any{"reason": "no changes to run for"})
return
}
runs := incrementRuns(ctx, &resp.Diagnostics, req.Private.GetKey, resp.Private.SetKey)
if resp.Diagnostics.HasError() {
return
}
ctx = 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+navigatorRunTimeoutOverhead)
defer cancel()
var runData navigatorRunData
resp.Diagnostics.Append(data.Value(ctx, false, r.opts, runs, state.Inventory.ValueStringPointer(), &runData)...)
if resp.Diagnostics.HasError() {
return
}
runData.operation = terraformOpUpdate
runData.config.Settings.Timeout = timeout
run(ctx, &resp.Diagnostics, &runData)
resp.Diagnostics.Append(data.Set(ctx, runData)...)
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", map[string]any{"reason": "run_on_destroy disabled"})
return
}
runs := incrementRuns(ctx, &resp.Diagnostics, req.Private.GetKey, resp.Private.SetKey)
if resp.Diagnostics.HasError() {
return
}
ctx = 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+navigatorRunTimeoutOverhead)
defer cancel()
var runData navigatorRunData
resp.Diagnostics.Append(data.Value(ctx, true, r.opts, runs, nil, &runData)...)
if resp.Diagnostics.HasError() {
return
}
runData.operation = terraformOpDelete
runData.config.Settings.Timeout = timeout
run(ctx, &resp.Diagnostics, &runData)
}
package provider
import (
"maps"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"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/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/planmodifier"
"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/marshallford/terraform-provider-ansible/pkg/ansible"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible/navigator"
)
func navigatorRunDescription(target surface) attrDescription {
preamble := "Run an Ansible playbook."
switch target {
case surfaceResource, surfaceAction:
case surfaceDataSource:
preamble = "Run an Ansible playbook to gather information. It is recommended to only run playbooks without observable side effects."
case surfaceEphemeral:
preamble = "Run an Ansible playbook to gather temporary and likely sensitive information. It is recommended to only run playbooks without observable side effects."
}
return describe("%s Requires `%s` and a container engine to run within an execution environment (EE).", preamble, navigator.Program)
}
func playbookDescription() attrDescription {
return describe("Ansible [playbook](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html) contents (YAML).")
}
func inventoryDescription(target surface) attrDescription {
description := describe("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)
if target != surfaceResource {
return description
}
return description.append("In addition, the environment variable `%s` is set to the path of the last applied inventory when the resource is updated.", navigatorRunPrevInventoryEnvVar)
}
func environmentVariablesSetDescription(target surface) attrDescription {
description := describe("Environment variables to be [set](https://ansible.readthedocs.io/projects/navigator/settings/#set-environment-variable) within the execution environment.")
operation := terraformOpRead
switch target {
case surfaceResource:
return description.append("`%s` is automatically set to the current CRUD operation (%s).", navigatorRunOperationEnvVar, wrapElementsJoin(terraformOps{terraformOpCreate, terraformOpUpdate, terraformOpDelete}.Strings(), "`"))
case surfaceDataSource:
case surfaceEphemeral:
operation = terraformOpOpen
case surfaceAction:
operation = terraformOpInvoke
}
return description.append("`%s` is automatically set to `%s`.", navigatorRunOperationEnvVar, operation)
}
func navigatorRunAttributes(target surface) map[string]schema.Attribute {
descriptions := map[string]attrDescription{
"playbook": playbookDescription(),
"working_directory": describe("Directory in which `%s` runs. 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`.", navigator.Program, defaultNavigatorRunWorkingDir),
"execution_environment": describe("[Execution environment](https://ansible.readthedocs.io/en/latest/getting_started_ee/index.html) (EE) related configuration."),
"ansible_navigator_binary": describe("Path to the `%s` binary. By default `$PATH` is searched.", navigator.Program),
"ansible_options": describe("Ansible [playbook](https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html) run related configuration."),
"timezone": describe("IANA time zone, use `local` for the system time zone. Defaults to `%s`.", defaultNavigatorRunTimezone),
"artifact_queries": describe("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": describe("UUID."),
"command": describe("Generated `%s` run command. Useful for troubleshooting.", navigator.Program),
}
attributes := map[string]schema.Attribute{
"playbook": schema.StringAttribute{
Description: descriptions["playbook"].Description,
MarkdownDescription: descriptions["playbook"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"inventory": schema.StringAttribute{
Description: inventoryDescription(target).Description,
MarkdownDescription: inventoryDescription(target).MarkdownDescription,
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"working_directory": schema.StringAttribute{
Description: descriptions["working_directory"].Description,
MarkdownDescription: descriptions["working_directory"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.stringDefault(defaultNavigatorRunWorkingDir),
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"execution_environment": schema.SingleNestedAttribute{
Description: descriptions["execution_environment"].Description,
MarkdownDescription: descriptions["execution_environment"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.objectDefault(ExecutionEnvironmentModel{}.Defaults()),
Attributes: executionEnvironmentAttributes(target),
},
"ansible_navigator_binary": schema.StringAttribute{
Description: descriptions["ansible_navigator_binary"].Description,
MarkdownDescription: descriptions["ansible_navigator_binary"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"ansible_options": schema.SingleNestedAttribute{
Description: descriptions["ansible_options"].Description,
MarkdownDescription: descriptions["ansible_options"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.objectDefault(AnsibleOptionsModel{}.Defaults()),
Attributes: ansibleOptionsAttributes(target),
},
"timezone": schema.StringAttribute{
Description: descriptions["timezone"].Description,
MarkdownDescription: descriptions["timezone"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.stringDefault(defaultNavigatorRunTimezone),
Validators: []validator.String{
stringIsIANATimezone(),
},
},
}
if target == surfaceResource {
maps.Copy(attributes, navigatorRunResourceAttributes())
}
// The action reports playbook output as progress events rather than state.
if target != surfaceAction {
maps.Copy(attributes, map[string]schema.Attribute{
"artifact_queries": schema.MapNestedAttribute{
Description: descriptions["artifact_queries"].Description,
MarkdownDescription: descriptions["artifact_queries"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: artifactQueryAttributes(),
},
},
"id": schema.StringAttribute{
Description: descriptions["id"].Description,
MarkdownDescription: descriptions["id"].MarkdownDescription,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"command": schema.StringAttribute{
Description: descriptions["command"].Description,
MarkdownDescription: descriptions["command"].MarkdownDescription,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
})
}
return attributes
}
func executionEnvironmentAttributes(target surface) map[string]schema.Attribute {
descriptions := map[string]attrDescription{
"container_engine": describe("[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(navigator.AllContainerEngines().Strings(), "`"), defaultNavigatorRunContainerEngine),
"enabled": describe("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": describe("Existing environment variables to be [passed](https://ansible.readthedocs.io/projects/navigator/settings/#pass-environment-variable) through to and set within the execution environment."),
"image": describe("Name of the execution environment container [image](https://ansible.readthedocs.io/projects/navigator/settings/#execution-environment-image). Defaults to `%s`.", defaultNavigatorRunImage),
"pull_arguments": describe("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": describe("Container image [pull policy](https://ansible.readthedocs.io/projects/navigator/settings/#pull-policy). Defaults to `%s`.", defaultNavigatorRunPullPolicy),
"container_options": describe("[Extra parameters](https://ansible.readthedocs.io/projects/navigator/settings/#container-options) passed to the container engine command."),
}
return map[string]schema.Attribute{
"container_engine": schema.StringAttribute{
Description: descriptions["container_engine"].Description,
MarkdownDescription: descriptions["container_engine"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.stringDefault(defaultNavigatorRunContainerEngine),
Validators: []validator.String{
stringvalidator.OneOf(navigator.AllContainerEngines().Strings()...),
},
},
"enabled": schema.BoolAttribute{
Description: descriptions["enabled"].Description,
MarkdownDescription: descriptions["enabled"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.boolDefault(defaultNavigatorRunEEEnabled),
},
"environment_variables_pass": schema.ListAttribute{
Description: descriptions["environment_variables_pass"].Description,
MarkdownDescription: descriptions["environment_variables_pass"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsEnvVarName()),
},
},
"environment_variables_set": schema.MapAttribute{
Description: environmentVariablesSetDescription(target).Description,
MarkdownDescription: environmentVariablesSetDescription(target).MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.Map{
mapvalidator.KeysAre(stringIsEnvVarName()),
},
},
"image": schema.StringAttribute{
Description: descriptions["image"].Description,
MarkdownDescription: descriptions["image"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.stringDefault(defaultNavigatorRunImage),
Validators: []validator.String{
stringIsContainerImageName(),
},
},
"pull_arguments": schema.ListAttribute{
Description: descriptions["pull_arguments"].Description,
MarkdownDescription: descriptions["pull_arguments"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"pull_policy": schema.StringAttribute{
Description: descriptions["pull_policy"].Description,
MarkdownDescription: descriptions["pull_policy"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
Default: target.stringDefault(defaultNavigatorRunPullPolicy),
Validators: []validator.String{
stringvalidator.OneOf(navigator.AllPullPolicies().Strings()...),
},
},
"container_options": schema.ListAttribute{
Description: descriptions["container_options"].Description,
MarkdownDescription: descriptions["container_options"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
}
}
func ansibleOptionsAttributes(target surface) map[string]schema.Attribute {
descriptions := map[string]attrDescription{
"extra_vars": describe("Set additional [variables](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_variables.html#defining-variables-at-runtime) (YAML)."),
"force_handlers": describe("Run handlers even if a task fails."),
"skip_tags": describe("Only run plays and tasks whose tags do not match these values."),
"start_at_task": describe("Start the playbook at the task matching this name."),
"limit": describe("Further limit selected hosts to an additional pattern."),
"tags": describe("Only run plays and tasks tagged with these values."),
"private_keys": describe("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": describe("SSH known host entries. Ansible variable `%s` set to path of `known_hosts` file and SSH option `UserKnownHostsFile` must be configured to that path. Defaults to all of the `known_hosts` entries recorded.", ansible.SSHKnownHostsFileVar),
"host_key_checking": describe("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.", navigator.Program, ansible.RunnerDefaultHostKeyChecking),
}
return map[string]schema.Attribute{
"extra_vars": schema.StringAttribute{
Description: descriptions["extra_vars"].Description,
MarkdownDescription: descriptions["extra_vars"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"force_handlers": schema.BoolAttribute{
Description: descriptions["force_handlers"].Description,
MarkdownDescription: descriptions["force_handlers"].MarkdownDescription,
Optional: true,
},
"skip_tags": schema.ListAttribute{
Description: descriptions["skip_tags"].Description,
MarkdownDescription: descriptions["skip_tags"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"start_at_task": schema.StringAttribute{
Description: descriptions["start_at_task"].Description,
MarkdownDescription: descriptions["start_at_task"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"limit": schema.ListAttribute{
Description: descriptions["limit"].Description,
MarkdownDescription: descriptions["limit"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"tags": schema.ListAttribute{
Description: descriptions["tags"].Description,
MarkdownDescription: descriptions["tags"].MarkdownDescription,
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
},
},
"private_keys": schema.ListNestedAttribute{
Description: descriptions["private_keys"].Description,
MarkdownDescription: descriptions["private_keys"].MarkdownDescription,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: privateKeyAttributes(target),
},
},
"known_hosts": schema.ListAttribute{
Description: descriptions["known_hosts"].Description,
MarkdownDescription: descriptions["known_hosts"].MarkdownDescription,
Optional: true,
Computed: target.allowsComputed(),
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsSSHKnownHost()),
},
},
"host_key_checking": schema.BoolAttribute{
Description: descriptions["host_key_checking"].Description,
MarkdownDescription: descriptions["host_key_checking"].MarkdownDescription,
Optional: true,
},
}
}
func privateKeyAttributes(target surface) map[string]schema.Attribute {
descriptions := map[string]attrDescription{
"name": describe("Key name."),
"data": describe("Key data."),
}
return map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: descriptions["name"].Description,
MarkdownDescription: descriptions["name"].MarkdownDescription,
Required: true,
Validators: []validator.String{
stringIsSSHPrivateKeyName(),
},
},
"data": schema.StringAttribute{
Description: descriptions["data"].Description,
MarkdownDescription: descriptions["data"].MarkdownDescription,
Required: true,
Sensitive: target.allowsSensitive(),
Validators: []validator.String{
stringIsSSHPrivateKey(),
},
},
}
}
func artifactQueryAttributes() map[string]schema.Attribute {
descriptions := map[string]attrDescription{
"jq_filter": describe("`jq` filter. Example: `.status, .stdout`."),
"results": describe("Results of the `jq` filter in JSON format."),
}
return map[string]schema.Attribute{
"jq_filter": schema.StringAttribute{
Description: descriptions["jq_filter"].Description,
MarkdownDescription: 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: descriptions["results"].Description,
MarkdownDescription: descriptions["results"].MarkdownDescription,
Computed: true,
ElementType: jsontypes.NormalizedType{},
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
}
}
func navigatorRunResourceAttributes() map[string]schema.Attribute {
descriptions := map[string]attrDescription{
"run_on_destroy": describe("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, terraformOpDelete, defaultNavigatorRunOnDestroy),
"destroy_playbook": playbookDescription().append("Only run on destroy (`run_on_destroy` must be `true`)."),
"triggers": describe("Trigger various behaviors via arbitrary values."),
}
triggers := map[string]attrDescription{
"run": describe("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."),
"exclusive_run": describe("When non-null, only changes to this value will run the playbook again. All other changes are ignored, the exception being resource destruction or replacement. Provides fine-grained control for advanced use cases."),
"replace": describe("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`."),
"known_hosts": describe("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."),
}
return map[string]schema.Attribute{
"run_on_destroy": schema.BoolAttribute{
Description: descriptions["run_on_destroy"].Description,
MarkdownDescription: descriptions["run_on_destroy"].MarkdownDescription,
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultNavigatorRunOnDestroy),
},
"destroy_playbook": schema.StringAttribute{
Description: descriptions["destroy_playbook"].Description,
MarkdownDescription: descriptions["destroy_playbook"].MarkdownDescription,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringIsYAML(),
},
},
"triggers": schema.SingleNestedAttribute{
Description: descriptions["triggers"].Description,
MarkdownDescription: descriptions["triggers"].MarkdownDescription,
Optional: true,
Attributes: map[string]schema.Attribute{
"run": schema.DynamicAttribute{
Description: triggers["run"].Description,
MarkdownDescription: triggers["run"].MarkdownDescription,
Optional: true,
},
"exclusive_run": schema.DynamicAttribute{
Description: triggers["exclusive_run"].Description,
MarkdownDescription: triggers["exclusive_run"].MarkdownDescription,
Optional: true,
},
"replace": schema.DynamicAttribute{
Description: triggers["replace"].Description,
MarkdownDescription: triggers["replace"].MarkdownDescription,
Optional: true,
PlanModifiers: []planmodifier.Dynamic{
dynamicplanmodifier.RequiresReplace(),
},
},
"known_hosts": schema.DynamicAttribute{
Description: triggers["known_hosts"].Description,
MarkdownDescription: triggers["known_hosts"].MarkdownDescription,
Optional: true,
},
},
},
}
}
package provider
import (
"context"
"fmt"
"os"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/action"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/function"
"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"
"github.com/spf13/afero"
)
const defaultProviderPersistRunDir = false
var (
_ provider.Provider = (*AnsibleProvider)(nil)
_ provider.ProviderWithEphemeralResources = (*AnsibleProvider)(nil)
_ provider.ProviderWithActions = (*AnsibleProvider)(nil)
_ provider.ProviderWithFunctions = (*AnsibleProvider)(nil)
)
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.CheckDirectory(afero.NewOsFs(), 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
resp.ActionData = &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 (p *AnsibleProvider) Actions(_ context.Context) []func() action.Action {
return []func() action.Action{
NewNavigatorRunAction,
}
}
func (p *AnsibleProvider) Functions(_ context.Context) []func() function.Function {
return []func() function.Function{
NewSSHArgsFunction,
NewSSHKnownHostFunction,
}
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &AnsibleProvider{
version: version,
}
}
}
package provider
import (
"context"
"fmt"
"regexp"
"strings"
"time"
actionTimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/action/timeouts"
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/action"
aschema "github.com/hashicorp/terraform-plugin-framework/action/schema"
"github.com/hashicorp/terraform-plugin-framework/datasource"
dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
eschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/path"
"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/defaults"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/types"
)
const (
diagDetailPrefix = "Underlying error details"
)
type attrDescription struct {
Description string
MarkdownDescription string
}
var markdownLink = regexp.MustCompile(`\[([^\]]*)\]\([^)]*\)`)
func describe(markdown string, args ...any) attrDescription {
if len(args) > 0 {
markdown = fmt.Sprintf(markdown, args...)
}
return attrDescription{
Description: strings.ReplaceAll(markdownLink.ReplaceAllString(markdown, "$1"), "`", "'"),
MarkdownDescription: markdown,
}
}
func (d attrDescription) append(markdown string, args ...any) attrDescription {
appended := describe(markdown, args...)
return attrDescription{
Description: d.Description + " " + appended.Description,
MarkdownDescription: d.MarkdownDescription + " " + appended.MarkdownDescription,
}
}
type providerOptions struct {
BaseRunDirectory string
PersistRunDirectory bool
}
type (
terraformOp string
terraformOps []terraformOp
)
const (
terraformOpCreate terraformOp = "create"
terraformOpRead terraformOp = "read"
terraformOpUpdate terraformOp = "update"
terraformOpDelete terraformOp = "delete"
terraformOpOpen terraformOp = "open"
terraformOpInvoke terraformOp = "invoke"
)
func (op terraformOp) String() string {
return string(op)
}
func (ops terraformOps) Strings() []string {
output := make([]string, 0, len(ops))
for _, element := range ops {
output = append(output, element.String())
}
return output
}
type surface int
const (
surfaceResource surface = iota
surfaceDataSource
surfaceEphemeral
surfaceAction
)
func (s surface) allowsComputed() bool {
return s != surfaceAction
}
func (s surface) allowsSensitive() bool {
return s != surfaceAction
}
func (s surface) stringDefault(value string) defaults.String { //nolint:ireturn
if s == surfaceAction {
return nil
}
return stringdefault.StaticString(value)
}
func (s surface) boolDefault(value bool) defaults.Bool { //nolint:ireturn
if s == surfaceAction {
return nil
}
return booldefault.StaticBool(value)
}
func (s surface) objectDefault(value types.Object) defaults.Object { //nolint:ireturn
if s == surfaceAction {
return nil
}
return objectdefault.StaticValue(value)
}
func dataSourceAttributes(attributes map[string]schema.Attribute) map[string]dschema.Attribute {
converted := make(map[string]dschema.Attribute, len(attributes))
for name, attribute := range attributes {
converted[name] = attribute
}
return converted
}
func ephemeralAttributes(attributes map[string]schema.Attribute) map[string]eschema.Attribute {
converted := make(map[string]eschema.Attribute, len(attributes))
for name, attribute := range attributes {
converted[name] = attribute
}
return converted
}
func actionAttributes(attributes map[string]schema.Attribute) map[string]aschema.Attribute {
converted := make(map[string]aschema.Attribute, len(attributes))
for name, attribute := range attributes {
converted[name] = attribute
}
return converted
}
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)
case terraformOpOpen, terraformOpInvoke:
return defaultTimeout, nil
}
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 terraformOperationActionTimeout(ctx context.Context, value actionTimeouts.Value, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) {
return value.Invoke(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 configureActionClient(req action.ConfigureRequest, resp *action.ConfigureResponse) (*providerOptions, bool) {
if req.ProviderData == nil {
return nil, false
}
opts, ok := req.ProviderData.(*providerOptions)
if !ok {
summary, detail := unexpectedConfigureType("Action", 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 {
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"
"errors"
"fmt"
"path/filepath"
"time"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"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"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible/navigator"
)
const (
navigatorRunName = "terraform"
navigatorRunExtraVarsFileName = "terraform.yaml"
navigatorRunPrevInventoryName = "previous-terraform"
navigatorRunDir = "tf-ansible-navigator-run"
navigatorRunOperationEnvVar = "ANSIBLE_TF_OPERATION"
navigatorRunInventoryEnvVar = "ANSIBLE_TF_INVENTORY"
navigatorRunPrevInventoryEnvVar = "ANSIBLE_TF_PREVIOUS_INVENTORY"
navigatorRunTimeoutOverhead = 5 * time.Second
defaultNavigatorRunWorkingDir = "."
defaultNavigatorRunTimeout = 10 * time.Minute
defaultNavigatorRunContainerEngine = string(navigator.ContainerEngineAuto)
defaultNavigatorRunEEEnabled = true
defaultNavigatorRunImage = "ghcr.io/ansible/community-ansible-dev-tools:v26.7.1"
defaultNavigatorRunPullPolicy = string(navigator.PullPolicyTag)
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 navigatorRunData struct {
hostDir string
config navigator.RunConfig
operation terraformOp
persistDir bool
playbookArtifactQueries map[string]ansible.PlaybookArtifactQuery
userArtifactQueries bool
knownHosts []ansible.KnownHost
command string
}
func (rd *navigatorRunData) Load(ctx context.Context, common NavigatorRunCommonModel) diag.Diagnostics {
var diags diag.Diagnostics
rd.config.WorkingDir = common.WorkingDirectory.ValueString()
rd.config.Binary = common.AnsibleNavigatorBinary.ValueString()
rd.config.Playbook = common.Playbook.ValueString()
rd.config.Inventories = []ansible.Inventory{{Name: navigatorRunName, Contents: common.Inventory.ValueString()}}
rd.config.Settings.Timezone = common.Timezone.ValueString()
var eeModel ExecutionEnvironmentModel
diags.Append(common.ExecutionEnvironment.As(ctx, &eeModel, basetypes.ObjectAsOptions{})...)
diags.Append(eeModel.Value(ctx, &rd.config.Settings.ExecutionEnvironment)...)
var optsModel AnsibleOptionsModel
diags.Append(common.AnsibleOptions.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Value(ctx, &rd.config.Options)...)
if !optsModel.ExtraVars.IsNull() {
rd.config.ExtraVars = []ansible.ExtraVarsFile{{Name: navigatorRunExtraVarsFileName, Contents: optsModel.ExtraVars.ValueString()}}
}
var privateKeysModel []PrivateKeyModel
if !optsModel.PrivateKeys.IsNull() {
diags.Append(optsModel.PrivateKeys.ElementsAs(ctx, &privateKeysModel, false)...)
}
rd.config.PrivateKeys = make([]ansible.PrivateKey, 0, len(privateKeysModel))
for _, model := range privateKeysModel {
var key ansible.PrivateKey
diags.Append(model.Value(ctx, &key)...)
rd.config.PrivateKeys = append(rd.config.PrivateKeys, key)
}
var knownHosts []string
if !optsModel.KnownHosts.IsUnknown() {
diags.Append(optsModel.KnownHosts.ElementsAs(ctx, &knownHosts, false)...)
}
rd.config.KnownHosts = knownHosts
rd.config.UseKnownHosts = optsModel.KnownHosts.IsUnknown() || len(optsModel.KnownHosts.Elements()) > 0
rd.config.HostKeyChecking = optsModel.HostKeyChecking.ValueBool()
if optsModel.HostKeyChecking.IsNull() {
rd.config.HostKeyChecking = ansible.RunnerDefaultHostKeyChecking
}
return diags
}
func (rd navigatorRunData) Store(ctx context.Context, command *types.String, ansibleOpts *types.Object, artifactQueries *types.Map) diag.Diagnostics {
var diags diag.Diagnostics
*command = types.StringValue(rd.command)
var optsModel AnsibleOptionsModel
diags.Append(ansibleOpts.As(ctx, &optsModel, basetypes.ObjectAsOptions{})...)
diags.Append(optsModel.Set(ctx, rd)...)
optsResults, newDiags := types.ObjectValueFrom(ctx, AnsibleOptionsModel{}.AttrTypes(), optsModel)
diags.Append(newDiags...)
*ansibleOpts = optsResults
var queriesModel map[string]ArtifactQueryModel
diags.Append(artifactQueries.ElementsAs(ctx, &queriesModel, false)...)
for name, model := range queriesModel {
diags.Append(model.Set(ctx, rd.playbookArtifactQueries[name])...)
queriesModel[name] = model
}
queriesValue, newDiags := types.MapValueFrom(ctx, types.ObjectType{AttrTypes: ArtifactQueryModel{}.AttrTypes()}, queriesModel)
diags.Append(newDiags...)
*artifactQueries = queriesValue
return diags
}
func (rd navigatorRunData) artifactQueryPath(name string) path.Path {
if !rd.userArtifactQueries {
return path.Empty()
}
return path.Root("artifact_queries").AtMapKey(name)
}
func preflightCheckPath(check navigator.PreflightCheck) path.Path {
switch check {
case navigator.CheckWorkingDir:
return path.Root("working_directory")
case navigator.CheckContainerEngine:
return path.Root("execution_environment").AtName("container_engine")
case navigator.CheckPlaybook:
return path.Root("execution_environment").AtName("enabled")
case navigator.CheckNavigatorResolve, navigator.CheckNavigatorBinary:
return path.Root("ansible_navigator_binary")
}
return path.Empty()
}
func setupStepPath(step navigator.SetupStep) path.Path {
switch step {
case navigator.SetupPlaybook:
return path.Root("playbook")
case navigator.SetupInventories:
return path.Root("inventory")
case navigator.SetupExtraVars:
return path.Root("ansible_options").AtName("extra_vars")
case navigator.SetupPrivateKeys:
return path.Root("ansible_options").AtName("private_keys")
case navigator.SetupKnownHosts:
return path.Root("ansible_options").AtName("known_hosts")
case navigator.SetupDir, navigator.SetupSettings:
return path.Empty()
}
return path.Empty()
}
//nolint:cyclop
func run(ctx context.Context, diags *diag.Diagnostics, runData *navigatorRunData) {
navRun := navigator.NewRun(runData.hostDir, runData.config)
ctx = tflog.SetField(ctx, "operation", runData.operation.String())
ctx = tflog.SetField(ctx, "mode", navRun.Mode().String())
ctx = tflog.SetField(ctx, "workingDir", runData.config.WorkingDir)
ctx = tflog.SetField(ctx, "hostDir", navRun.HostDir())
ctx = tflog.SetField(ctx, "playbookDir", navRun.PlaybookDir())
tflog.Debug(ctx, "starting run")
defer func() {
if !runData.persistDir {
err := navRun.Cleanup()
addWarning(diags, "Run not cleaned up", err)
}
}()
navRun.SetEnv(navigatorRunOperationEnvVar, runData.operation.String())
navRun.SetEnv(navigatorRunInventoryEnvVar, navRun.InventoryPath(navigatorRunName))
if runData.operation == terraformOpUpdate {
navRun.SetEnv(navigatorRunPrevInventoryEnvVar, navRun.InventoryPath(navigatorRunPrevInventoryName))
}
tflog.Trace(ctx, "running preflight checks")
if err := navRun.Preflight(ctx); err != nil {
for _, preflightErr := range unwrapJoinedErrors(err) {
var typed *navigator.PreflightError
if errors.As(preflightErr, &typed) {
addPathError(diags, preflightCheckPath(typed.Check), "Preflight check failed", typed)
continue
}
addError(diags, "Preflight check failed", preflightErr)
}
}
tflog.Trace(ctx, "setting up run directory")
if err := navRun.Setup(); err != nil {
for _, setupErr := range unwrapJoinedErrors(err) {
var typed *navigator.SetupError
if errors.As(setupErr, &typed) {
addPathError(diags, setupStepPath(typed.Step), "Setup failed", typed)
continue
}
addError(diags, "Setup failed", setupErr)
}
}
if diags.HasError() {
return
}
tflog.Trace(ctx, fmt.Sprintf("executing %s", navigator.Program))
if err := navRun.Execute(ctx); err != nil {
runData.command = navRun.Command.String()
summary := "Ansible navigator run failed"
if navRun.Status == ansible.StatusTimeout {
summary = "Ansible navigator run timed out"
}
addError(diags, summary, fmt.Errorf("%w\n\nOutput:\n%s", err, navRun.Output))
return
}
runData.command = navRun.Command.String()
tflog.Trace(ctx, "querying playbook artifact")
if err := navRun.Query(runData.playbookArtifactQueries); err != nil {
for _, queryErr := range unwrapJoinedErrors(err) {
var typed *navigator.QueryError
if errors.As(queryErr, &typed) {
addPathError(diags, runData.artifactQueryPath(typed.Name), "Playbook artifact query failed", typed)
continue
}
addError(diags, "Playbook artifact queries failed", queryErr)
}
}
if runData.config.UseKnownHosts {
tflog.Trace(ctx, "reading known hosts")
knownHosts, err := navRun.ReadKnownHosts()
if err != nil {
addPathError(diags, path.Root("ansible_options").AtName("known_hosts"), "Failed to read known hosts", err)
}
runData.knownHosts = knownHosts
}
tflog.Debug(ctx, "run complete")
}
func unwrapJoinedErrors(err error) []error {
if err == nil {
return nil
}
if joined, ok := err.(interface{ Unwrap() []error }); ok {
return joined.Unwrap()
}
return []error{err}
}
func navigatorRunDirPath(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/function"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ function.Function = (*SSHArgsFunction)(nil)
)
func NewSSHArgsFunction() function.Function { //nolint:ireturn
return &SSHArgsFunction{}
}
type SSHArgsFunction struct{}
func (f *SSHArgsFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "ssh_args"
}
func (f *SSHArgsFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "SSH args for configuring Ansible to integrate with provider managed known hosts.",
Description: "SSH command line arguments for configuring Ansible to integrate with provider managed known hosts. Set or append to the 'ansible_ssh_common_args' Ansible variable or environment variable.",
MarkdownDescription: "SSH command line arguments for configuring Ansible to integrate with provider managed known hosts. Set or append to the `ansible_ssh_common_args` Ansible variable or environment variable.",
Parameters: []function.Parameter{
function.BoolParameter{
Name: "accept_new",
Description: "Accept and add new host keys ('StrictHostKeyChecking=accept_new') or only allow connections to hosts whose key(s) are already present ('StrictHostKeyChecking=yes').",
MarkdownDescription: "Accept and add new host keys (`StrictHostKeyChecking=accept_new`) or only allow connections to hosts whose key(s) are already present (`StrictHostKeyChecking=yes`).",
},
},
Return: function.StringReturn{},
}
}
func (f *SSHArgsFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var acceptNew bool
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &acceptNew))
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, ansible.SSHArgs(acceptNew)))
}
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)
var (
_ function.Function = (*SSHKnownHostFunction)(nil)
)
func NewSSHKnownHostFunction() function.Function { //nolint:ireturn
return &SSHKnownHostFunction{}
}
type SSHKnownHostFunction struct{}
func (f *SSHKnownHostFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "ssh_known_host"
}
func (f *SSHKnownHostFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Format a public key and addresses into a known hosts entry.",
Description: "Format a public key and addresses into a known hosts entry/line suitable for use in an SSH known hosts file.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "public_key",
Description: "Public key data in the authorized keys format.",
},
},
VariadicParameter: function.StringParameter{
Name: "addresses",
Description: "Addresses to associate with the public key. Can be one or more hostnames or IP addresses with an optional port.",
},
Return: function.StringReturn{},
}
}
func (f *SSHKnownHostFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var publicKey string
var addresses []string
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &publicKey, &addresses))
entry, err := ansible.KnownHostsLine(addresses, publicKey)
if err != nil {
resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(err.Error()))
return
}
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, entry))
}
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible/navigator"
)
type stringIsSSHPrivateKeyValidator struct{}
var _ validator.String = (*stringIsSSHPrivateKeyValidator)(nil)
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{}
}
func StringIsSSHPrivateKey() validator.String { //nolint:ireturn
return stringIsSSHPrivateKey()
}
type stringIsSSHPrivateKeyNameValidator struct{}
var _ validator.String = (*stringIsSSHPrivateKeyNameValidator)(nil)
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{}
}
func StringIsSSHPrivateKeyName() validator.String { //nolint:ireturn
return stringIsSSHPrivateKeyName()
}
type stringIsSSHKnownHostValidator struct{}
var _ validator.String = (*stringIsSSHKnownHostValidator)(nil)
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{}
}
func StringIsSSHKnownHost() validator.String { //nolint:ireturn
return stringIsSSHKnownHost()
}
type stringIsEnvVarNameValidator struct{}
var _ validator.String = (*stringIsEnvVarNameValidator)(nil)
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{}
}
func StringIsEnvVarName() validator.String { //nolint:ireturn
return stringIsEnvVarName()
}
type stringIsYAMLValidator struct{}
var _ validator.String = (*stringIsYAMLValidator)(nil)
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{}
}
func StringIsYAML() validator.String { //nolint:ireturn
return stringIsYAML()
}
type stringIsIANATimezoneValidator struct{}
var _ validator.String = (*stringIsIANATimezoneValidator)(nil)
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 := navigator.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{}
}
func StringIsIANATimezone() validator.String { //nolint:ireturn
return stringIsIANATimezone()
}
type stringIsJQFilterValidator struct{}
var _ validator.String = (*stringIsJQFilterValidator)(nil)
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{}
}
func StringIsJQFilter() validator.String { //nolint:ireturn
return stringIsJQFilter()
}
type stringIsContainerImageNameValidator struct{}
var _ validator.String = (*stringIsContainerImageNameValidator)(nil)
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 := navigator.ValidateContainerImageName(req.ConfigValue.ValueString())
addPathError(&resp.Diagnostics, req.Path, "Not a valid container image name", err)
}
func stringIsContainerImageName() stringIsContainerImageNameValidator {
return stringIsContainerImageNameValidator{}
}
func StringIsContainerImageName() validator.String { //nolint:ireturn
return stringIsContainerImageName()
}