Skip to content

Commit 6dda031

Browse files
committed
Add agent configuration resource
1 parent db09c21 commit 6dda031

File tree

9 files changed

+454
-0
lines changed

9 files changed

+454
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package agent_configuration
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
9+
"github.com/hashicorp/terraform-plugin-framework/resource"
10+
"github.com/hashicorp/terraform-plugin-log/tflog"
11+
)
12+
13+
func (r *resourceAgentConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
14+
var plan AgentConfiguration
15+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
16+
if resp.Diagnostics.HasError() {
17+
return
18+
}
19+
20+
kibana, err := r.client.GetKibanaOapiClient()
21+
if err != nil {
22+
resp.Diagnostics.AddError("Unable to get Kibana client", err.Error())
23+
return
24+
}
25+
26+
settings := make(map[string]string)
27+
resp.Diagnostics.Append(plan.Settings.ElementsAs(ctx, &settings, false)...)
28+
if resp.Diagnostics.HasError() {
29+
return
30+
}
31+
32+
agentConfig := kbapi.CreateUpdateAgentConfigurationJSONRequestBody{
33+
AgentName: plan.AgentName.ValueStringPointer(),
34+
Service: kbapi.APMUIServiceObject{
35+
Name: plan.ServiceName.ValueStringPointer(),
36+
Environment: plan.ServiceEnvironment.ValueStringPointer(),
37+
},
38+
Settings: settings,
39+
}
40+
41+
apiResp, err := kibana.API.CreateUpdateAgentConfiguration(ctx, &kbapi.CreateUpdateAgentConfigurationParams{}, agentConfig)
42+
if err != nil {
43+
resp.Diagnostics.AddError("Failed to create APM agent configuration", err.Error())
44+
return
45+
}
46+
defer apiResp.Body.Close()
47+
48+
if diags := utils.CheckHttpErrorFromFW(apiResp, "Failed to create APM agent configuration"); diags.HasError() {
49+
resp.Diagnostics.Append(diags...)
50+
return
51+
}
52+
53+
id := plan.SetIDFromService()
54+
tflog.Trace(ctx, fmt.Sprintf("Created APM agent configuration with ID: %s", id))
55+
56+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
57+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package agent_configuration
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
11+
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
12+
"github.com/hashicorp/terraform-plugin-framework/resource"
13+
"github.com/hashicorp/terraform-plugin-log/tflog"
14+
)
15+
16+
type deleteAgentConfigurationRequestBody struct {
17+
Service struct {
18+
Name string `json:"name"`
19+
Environment *string `json:"environment,omitempty"`
20+
} `json:"service"`
21+
}
22+
23+
func (r *resourceAgentConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
24+
var state AgentConfiguration
25+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
26+
if resp.Diagnostics.HasError() {
27+
return
28+
}
29+
30+
kibana, err := r.client.GetKibanaOapiClient()
31+
if err != nil {
32+
resp.Diagnostics.AddError("Unable to get Kibana client", err.Error())
33+
return
34+
}
35+
36+
idParts := strings.Split(state.ID.ValueString(), ":")
37+
serviceName := idParts[0]
38+
var serviceEnv *string
39+
if len(idParts) > 1 {
40+
serviceEnv = &idParts[1]
41+
}
42+
43+
deleteReqBody := deleteAgentConfigurationRequestBody{}
44+
deleteReqBody.Service.Name = serviceName
45+
deleteReqBody.Service.Environment = serviceEnv
46+
47+
bodyBytes, err := json.Marshal(deleteReqBody)
48+
if err != nil {
49+
resp.Diagnostics.AddError("Failed to serialize delete request body", err.Error())
50+
return
51+
}
52+
53+
apiResp, err := kibana.API.DeleteAgentConfigurationWithBody(ctx, &kbapi.DeleteAgentConfigurationParams{}, "application/json", bytes.NewReader(bodyBytes))
54+
if err != nil {
55+
resp.Diagnostics.AddError("Failed to delete APM agent configuration", err.Error())
56+
return
57+
}
58+
defer apiResp.Body.Close()
59+
60+
if diags := utils.CheckHttpErrorFromFW(apiResp, "Failed to delete APM agent configuration"); diags.HasError() {
61+
resp.Diagnostics.Append(diags...)
62+
return
63+
}
64+
65+
tflog.Trace(ctx, fmt.Sprintf("Deleted APM agent configuration with ID: %s", state.ID.ValueString()))
66+
67+
resp.State.RemoveResource(ctx)
68+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package agent_configuration
2+
3+
import (
4+
"strings"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/types"
7+
)
8+
9+
// AgentConfiguration holds the agent configuration.
10+
type AgentConfiguration struct {
11+
ID types.String `tfsdk:"id"`
12+
ServiceName types.String `tfsdk:"service_name"`
13+
ServiceEnvironment types.String `tfsdk:"service_environment"`
14+
AgentName types.String `tfsdk:"agent_name"`
15+
Settings types.Map `tfsdk:"settings"`
16+
}
17+
18+
func (ac *AgentConfiguration) SetIDFromService() string {
19+
parts := []string{ac.ServiceName.ValueString()}
20+
if !ac.ServiceEnvironment.IsNull() && !ac.ServiceEnvironment.IsUnknown() && ac.ServiceEnvironment.ValueString() != "" {
21+
parts = append(parts, ac.ServiceEnvironment.ValueString())
22+
}
23+
24+
id := strings.Join(parts, ":")
25+
ac.ID = types.StringValue(id)
26+
return id
27+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package agent_configuration
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
9+
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
10+
"github.com/hashicorp/terraform-plugin-framework/resource"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
)
13+
14+
func (r *resourceAgentConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
15+
var state AgentConfiguration
16+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
17+
if resp.Diagnostics.HasError() {
18+
return
19+
}
20+
21+
kibana, err := r.client.GetKibanaOapiClient()
22+
if err != nil {
23+
resp.Diagnostics.AddError("Unable to get Kibana client", err.Error())
24+
return
25+
}
26+
27+
apiResp, err := kibana.API.GetAgentConfigurationsWithResponse(ctx, &kbapi.GetAgentConfigurationsParams{})
28+
if err != nil {
29+
resp.Diagnostics.AddError("Failed to get APM agent configurations", err.Error())
30+
return
31+
}
32+
33+
if diags := utils.CheckHttpErrorFromFW(apiResp.HTTPResponse, "Failed to get APM agent configurations"); diags.HasError() {
34+
resp.Diagnostics.Append(diags...)
35+
return
36+
}
37+
38+
if apiResp.JSON200 == nil {
39+
resp.Diagnostics.AddError("Failed to get APM agent configurations from body", "Expected 200 response body to not be nil")
40+
return
41+
}
42+
43+
idFromState := state.ID.ValueString()
44+
var foundConfig *kbapi.APMUIAgentConfigurationObject
45+
for _, config := range *apiResp.JSON200.Configurations {
46+
if config.Service.Name == nil {
47+
continue
48+
}
49+
idFromAPI := createAgentConfigIDfromAPI(config)
50+
if idFromAPI == idFromState {
51+
foundConfig = &config
52+
break
53+
}
54+
}
55+
56+
if foundConfig == nil {
57+
resp.State.RemoveResource(ctx)
58+
return
59+
}
60+
61+
state.ID = types.StringValue(idFromState)
62+
state.ServiceName = types.StringPointerValue(foundConfig.Service.Name)
63+
state.ServiceEnvironment = types.StringPointerValue(foundConfig.Service.Environment)
64+
state.AgentName = types.StringPointerValue(foundConfig.AgentName)
65+
66+
stringSettings := make(map[string]interface{})
67+
if foundConfig.Settings != nil {
68+
for k, v := range foundConfig.Settings {
69+
stringSettings[k] = fmt.Sprintf("%v", v)
70+
}
71+
}
72+
73+
settings, diags := types.MapValueFrom(ctx, types.StringType, stringSettings)
74+
resp.Diagnostics.Append(diags...)
75+
if resp.Diagnostics.HasError() {
76+
return
77+
}
78+
state.Settings = settings
79+
80+
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
81+
}
82+
83+
func createAgentConfigIDfromAPI(config kbapi.APMUIAgentConfigurationObject) string {
84+
parts := []string{*config.Service.Name}
85+
if config.Service.Environment != nil && *config.Service.Environment != "" {
86+
parts = append(parts, *config.Service.Environment)
87+
}
88+
return strings.Join(parts, ":")
89+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package agent_configuration
2+
3+
import (
4+
"context"
5+
6+
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
7+
"github.com/hashicorp/terraform-plugin-framework/resource"
8+
)
9+
10+
// Ensure provider defined types fully satisfy framework interfaces.
11+
var _ resource.Resource = &resourceAgentConfiguration{}
12+
13+
func NewAgentConfigurationResource() resource.Resource {
14+
return &resourceAgentConfiguration{}
15+
}
16+
17+
type resourceAgentConfiguration struct {
18+
client *clients.ApiClient
19+
}
20+
21+
func (r *resourceAgentConfiguration) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
22+
resp.TypeName = req.ProviderTypeName + "_apm_agent_configuration"
23+
}
24+
25+
func (r *resourceAgentConfiguration) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
26+
client, diags := clients.ConvertProviderData(req.ProviderData)
27+
resp.Diagnostics.Append(diags...)
28+
r.client = client
29+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package agent_configuration_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
8+
tf_acctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
)
11+
12+
func TestAccResourceAgentConfiguration(t *testing.T) {
13+
serviceName := tf_acctest.RandStringFromCharSet(10, tf_acctest.CharSetAlphaNum)
14+
15+
resource.Test(t, resource.TestCase{
16+
PreCheck: func() { acctest.PreCheck(t) },
17+
ProtoV6ProviderFactories: acctest.Providers,
18+
Steps: []resource.TestStep{
19+
{
20+
Config: testAccResourceAgentConfigurationCreate(serviceName),
21+
Check: resource.ComposeTestCheckFunc(
22+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "service_name", serviceName),
23+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "service_environment", "production"),
24+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "agent_name", "go"),
25+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "settings.transaction_sample_rate", "0.5"),
26+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "settings.capture_body", "all"),
27+
),
28+
},
29+
{
30+
Config: testAccResourceAgentConfigurationUpdate(serviceName),
31+
Check: resource.ComposeTestCheckFunc(
32+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "service_name", serviceName),
33+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "service_environment", "production"),
34+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "agent_name", "java"),
35+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "settings.transaction_sample_rate", "0.8"),
36+
resource.TestCheckResourceAttr("elasticstack_apm_agent_configuration.test_config", "settings.capture_body", "off"),
37+
),
38+
},
39+
},
40+
})
41+
}
42+
43+
func testAccResourceAgentConfigurationCreate(serviceName string) string {
44+
return fmt.Sprintf(`
45+
provider "elasticstack" {
46+
kibana {}
47+
}
48+
49+
resource "elasticstack_apm_agent_configuration" "test_config" {
50+
service_name = "%s"
51+
service_environment = "production"
52+
agent_name = "go"
53+
settings = {
54+
"transaction_sample_rate" = "0.5"
55+
"capture_body" = "all"
56+
}
57+
}
58+
`, serviceName)
59+
}
60+
61+
func testAccResourceAgentConfigurationUpdate(serviceName string) string {
62+
return fmt.Sprintf(`
63+
provider "elasticstack" {
64+
kibana {}
65+
}
66+
67+
resource "elasticstack_apm_agent_configuration" "test_config" {
68+
service_name = "%s"
69+
service_environment = "production"
70+
agent_name = "java"
71+
settings = {
72+
"transaction_sample_rate" = "0.8"
73+
"capture_body" = "off"
74+
}
75+
}
76+
`, serviceName)
77+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package agent_configuration
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/resource"
7+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
8+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
)
12+
13+
func (r *resourceAgentConfiguration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
14+
resp.Schema = schema.Schema{
15+
Description: "Manages APM agent configuration.",
16+
17+
Attributes: map[string]schema.Attribute{
18+
"id": schema.StringAttribute{
19+
Computed: true,
20+
MarkdownDescription: "Internal identifier of the resource.",
21+
PlanModifiers: []planmodifier.String{
22+
stringplanmodifier.UseStateForUnknown(),
23+
},
24+
},
25+
"service_name": schema.StringAttribute{
26+
Description: "The name of the service.",
27+
Required: true,
28+
},
29+
"service_environment": schema.StringAttribute{
30+
Description: "The environment of the service.",
31+
Optional: true,
32+
},
33+
"agent_name": schema.StringAttribute{
34+
Description: "The agent name is used by the UI to determine which settings to display.",
35+
Optional: true,
36+
},
37+
"settings": schema.MapAttribute{
38+
Description: "Agent configuration settings.",
39+
Required: true,
40+
ElementType: types.StringType,
41+
},
42+
},
43+
}
44+
}

0 commit comments

Comments
 (0)