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)(nil) _ datasource.DataSourceWithConfigure = (*NavigatorRunDataSource)(nil) ) 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)(nil) _ ephemeral.EphemeralResourceWithConfigure = (*NavigatorRunEphemeralResource)(nil) ) 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)(nil) _ resource.ResourceWithConfigure = (*NavigatorRunResource)(nil) _ resource.ResourceWithModifyPlan = (*NavigatorRunResource)(nil) ) 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, }, "exclusive_run": schema.DynamicAttribute{ Description: "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.", 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 { if !r.TriggersAttr(plan, "exclusive_run").IsNull() { return !r.TriggersAttr(plan, "exclusive_run").Equal(r.TriggersAttr(state, "exclusive_run")) } // skip working_directory, ansible_navigator_binary, run_on_destroy, destroy_playbook, 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/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" ) const defaultProviderPersistRunDir = false var ( _ provider.Provider = (*AnsibleProvider)(nil) _ provider.ProviderWithEphemeralResources = (*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.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 (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" "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.5.2" 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/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" ) 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{} } 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{} } 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{} } 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{} } 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{} } 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 := 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{} 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{} } 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 := ansible.ValidateContainerImageName(req.ConfigValue.ValueString()) addPathError(&resp.Diagnostics, req.Path, "Not a valid container image name", err) } func stringIsContainerImageName() stringIsContainerImageNameValidator { return stringIsContainerImageNameValidator{} }