From ee600ffd7eb4890a6b916582a542e721c0d4f8d1 Mon Sep 17 00:00:00 2001 From: denys-octopus <102932057+denys-octopus@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:41:36 +1300 Subject: [PATCH] feat: Add workers data source (#818) --- docs/data-sources/workers.md | 70 +++++++ .../octopusdeploy_workers/data-source.tf | 10 + octopusdeploy_framework/datasource_workers.go | 72 +++++++ .../datasource_workers_test.go | 130 +++++++++++++ octopusdeploy_framework/framework_provider.go | 1 + octopusdeploy_framework/schemas/workers.go | 176 ++++++++++++++++++ 6 files changed, 459 insertions(+) create mode 100644 docs/data-sources/workers.md create mode 100644 examples/data-sources/octopusdeploy_workers/data-source.tf create mode 100644 octopusdeploy_framework/datasource_workers.go create mode 100644 octopusdeploy_framework/datasource_workers_test.go create mode 100644 octopusdeploy_framework/schemas/workers.go diff --git a/docs/data-sources/workers.md b/docs/data-sources/workers.md new file mode 100644 index 000000000..059e2128f --- /dev/null +++ b/docs/data-sources/workers.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "octopusdeploy_workers Data Source - terraform-provider-octopusdeploy" +subcategory: "" +description: |- + Provides information about existing workers. +--- + +# octopusdeploy_workers (Data Source) + +Provides information about existing workers. + +## Example Usage + +```terraform +data "octopusdeploy_workers" "example" { + communication_styles = ["TentaclePassive"] + health_statuses = ["Unavailable"] + ids = ["Workers-123"] + name = "Exact name" + partial_name = "Test" + skip = 5 + take = 100 + is_disabled = true +} +``` + + +## Schema + +### Optional + +- `communication_styles` (List of String) A filter to search by communication styles +- `health_statuses` (List of String) A filter to search by health statuses +- `ids` (List of String) A filter to search by a list of IDs. +- `is_disabled` (Boolean) +- `name` (String) The name of this resource. +- `partial_name` (String) A filter to search by a partial name. +- `skip` (Number) A filter to specify the number of items to skip in the response. +- `space_id` (String) The space ID associated with this workers. +- `take` (Number) A filter to specify the number of items to take (or return) in the response. + +### Read-Only + +- `id` (String) The unique ID for this resource. +- `workers` (Attributes List) (see [below for nested schema](#nestedatt--workers)) + + +### Nested Schema for `workers` + +Read-Only: + +- `account_id` (String) +- `communication_style` (String) +- `dotnet_platform` (String) +- `fingerprint` (String) +- `health_status` (String) +- `host` (String) +- `id` (String) The unique ID for this resource. +- `is_disabled` (Boolean) +- `machine_policy_id` (String) +- `name` (String) The name of this resource. +- `port` (Number) +- `proxy_id` (String) +- `space_id` (String) The space ID associated with this workers. +- `thumbprint` (String) +- `uri` (String) +- `worker_pool_ids` (List of String) + + diff --git a/examples/data-sources/octopusdeploy_workers/data-source.tf b/examples/data-sources/octopusdeploy_workers/data-source.tf new file mode 100644 index 000000000..9b8ecedfb --- /dev/null +++ b/examples/data-sources/octopusdeploy_workers/data-source.tf @@ -0,0 +1,10 @@ +data "octopusdeploy_workers" "example" { + communication_styles = ["TentaclePassive"] + health_statuses = ["Unavailable"] + ids = ["Workers-123"] + name = "Exact name" + partial_name = "Test" + skip = 5 + take = 100 + is_disabled = true +} \ No newline at end of file diff --git a/octopusdeploy_framework/datasource_workers.go b/octopusdeploy_framework/datasource_workers.go new file mode 100644 index 000000000..fcdf9ecf9 --- /dev/null +++ b/octopusdeploy_framework/datasource_workers.go @@ -0,0 +1,72 @@ +package octopusdeploy_framework + +import ( + "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/machines" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/workers" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "time" +) + +type workersDataSource struct { + *Config +} + +func NewWorkersDataSource() datasource.DataSource { + return &workersDataSource{} +} + +func (*workersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = util.GetTypeName("workers") +} + +func (e *workersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + e.Config = DataSourceConfiguration(req, resp) +} + +func (*workersDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schemas.WorkersSchema{}.GetDatasourceSchema() +} + +func (e *workersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var err error + var data schemas.WorkersDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + query := machines.WorkersQuery{ + Name: data.Name.ValueString(), + IDs: util.GetIds(data.IDs), + PartialName: data.PartialName.ValueString(), + Skip: util.GetNumber(data.Skip), + Take: util.GetNumber(data.Take), + CommunicationStyles: util.ExpandStringList(data.CommunicationStyle), + HealthStatuses: util.ExpandStringList(data.HealthStatuses), + IsDisabled: data.IsDisabled.ValueBool(), + } + + util.DatasourceReading(ctx, "workers", query) + + existingWorkers, err := workers.Get(e.Client, data.SpaceID.ValueString(), query) + if err != nil { + resp.Diagnostics.AddError("unable to load workers", err.Error()) + return + } + + util.DatasourceResultCount(ctx, "workers", len(existingWorkers.Items)) + + resources := []interface{}{} + for _, worker := range existingWorkers.Items { + resources = append(resources, schemas.FlattenWorker(worker)) + } + + data.Workers, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.WorkerObjectType()}, resources) + data.ID = types.StringValue("Workers " + time.Now().UTC().String()) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/octopusdeploy_framework/datasource_workers_test.go b/octopusdeploy_framework/datasource_workers_test.go new file mode 100644 index 000000000..100297be1 --- /dev/null +++ b/octopusdeploy_framework/datasource_workers_test.go @@ -0,0 +1,130 @@ +package octopusdeploy_framework + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccDataSourceWorkers(t *testing.T) { + localName := acctest.RandStringFromCharSet(50, acctest.CharSetAlpha) + prefix := fmt.Sprintf("data.octopusdeploy_workers.%s", localName) + sshFilter := `communication_styles = ["Ssh"]` + listeningFilter := `communication_styles = ["TentaclePassive"]` + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + PreCheck: func() { TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: configTestAccDataSourceWorkerResources(localName, ""), + }, + { + Config: configTestAccDataSourceWorkerResources(localName, ""), + Check: testAssertDataSourceWorkersEmpty(prefix), + }, + { + Config: configTestAccDataSourceWorkerResources(localName, sshFilter), + Check: testAssertDataSourceSSHWorkers(prefix), + }, + { + Config: configTestAccDataSourceWorkerResources(localName, listeningFilter), + Check: testAssertDataSourceListeningWorkers(prefix), + }, + }, + }) +} + +func testAssertDataSourceWorkersEmpty(prefix string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + testAssertWorkersDataSourceID(prefix), + resource.TestCheckResourceAttr(prefix, "workers.#", "2"), + ) +} + +func testAssertDataSourceSSHWorkers(prefix string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + testAssertWorkersDataSourceID(prefix), + resource.TestCheckResourceAttr(prefix, "workers.#", "1"), + resource.TestCheckResourceAttr(prefix, "workers.0.name", "First SSH worker"), + resource.TestCheckResourceAttr(prefix, "workers.0.host", "test.domain"), + resource.TestCheckResourceAttr(prefix, "workers.0.port", "4201"), + resource.TestCheckResourceAttr(prefix, "workers.0.fingerprint", "SHA256: 1234abcdef56789"), + resource.TestCheckResourceAttr(prefix, "workers.0.dotnet_platform", "linux-x64"), + ) +} + +func testAssertDataSourceListeningWorkers(prefix string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + testAssertWorkersDataSourceID(prefix), + resource.TestCheckResourceAttr(prefix, "workers.#", "1"), + resource.TestCheckResourceAttr(prefix, "workers.0.name", "Second listening worker"), + resource.TestCheckResourceAttr(prefix, "workers.0.uri", "https://domain.test/"), + resource.TestCheckResourceAttr(prefix, "workers.0.thumbprint", "abcdef"), + ) +} + +func testAssertWorkersDataSourceID(prefix string) resource.TestCheckFunc { + return func(s *terraform.State) error { + all := s.RootModule().Resources + dataSource, ok := all[prefix] + if !ok { + return fmt.Errorf("cannot find Workers data source: %s", prefix) + } + + if dataSource.Primary.ID == "" { + return fmt.Errorf("snapshot Workers source ID not set") + } + return nil + } +} + +func configTestAccDataSourceWorkerResources(localName string, dataSourceFilter string) string { + return fmt.Sprintf(` + resource "octopusdeploy_machine_policy" "policy_1" { + name = "Machine Policy One" + } + + resource "octopusdeploy_static_worker_pool" "pool_1" { + name = "Worker Pool One" + description = "First pool of listening workers" + sort_order = 99 + } + + resource "octopusdeploy_ssh_key_account" "account_1" { + name = "SSH Key Pair Account" + private_key_file = "[private_key_file]" + username = "[username]" + } + + resource "octopusdeploy_ssh_connection_worker" "worker_1" { + name = "First SSH worker" + machine_policy_id = octopusdeploy_machine_policy.policy_1.id + worker_pool_ids = [octopusdeploy_static_worker_pool.pool_1.id] + account_id = octopusdeploy_ssh_key_account.account_1.id + host = "test.domain" + port = 4201 + fingerprint = "SHA256: 1234abcdef56789" + dotnet_platform = "linux-x64" + } + + resource "octopusdeploy_listening_tentacle_worker" "worker_2" { + name = "Second listening worker" + machine_policy_id = octopusdeploy_machine_policy.policy_1.id + worker_pool_ids = [octopusdeploy_static_worker_pool.pool_1.id] + uri = "https://domain.test/" + thumbprint = "abcdef" + } + + data "octopusdeploy_workers" "%s" { + %s + } + `, + localName, + dataSourceFilter, + ) +} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index ab4962f55..b738e4ff7 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -84,6 +84,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func NewScriptModuleDataSource, NewTenantProjectDataSource, NewUsersDataSource, + NewWorkersDataSource, } } diff --git a/octopusdeploy_framework/schemas/workers.go b/octopusdeploy_framework/schemas/workers.go new file mode 100644 index 000000000..e9602d6dc --- /dev/null +++ b/octopusdeploy_framework/schemas/workers.go @@ -0,0 +1,176 @@ +package schemas + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/machines" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type WorkersSchema struct{} + +var _ EntitySchema = WorkersSchema{} + +func (f WorkersSchema) GetResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{} +} + +func (f WorkersSchema) GetDatasourceSchema() datasourceSchema.Schema { + return datasourceSchema.Schema{ + Description: "Provides information about existing workers.", + Attributes: map[string]datasourceSchema.Attribute{ + "ids": GetQueryIDsDatasourceSchema(), + "name": GetNameDatasourceSchema(false), + "partial_name": GetQueryPartialNameDatasourceSchema(), + "skip": GetQuerySkipDatasourceSchema(), + "take": GetQueryTakeDatasourceSchema(), + "space_id": GetSpaceIdDatasourceSchema("workers", false), + "communication_styles": datasourceSchema.ListAttribute{ + Description: "A filter to search by communication styles", + ElementType: types.StringType, + Optional: true, + }, + "health_statuses": datasourceSchema.ListAttribute{ + Description: "A filter to search by health statuses", + ElementType: types.StringType, + Optional: true, + }, + "is_disabled": GetBooleanDatasourceAttribute("", true), + + // response + "id": GetIdDatasourceSchema(true), + "workers": datasourceSchema.ListNestedAttribute{ + Computed: true, + Optional: false, + NestedObject: datasourceSchema.NestedAttributeObject{ + Attributes: map[string]datasourceSchema.Attribute{ + "id": GetIdDatasourceSchema(true), + "space_id": GetSpaceIdDatasourceSchema("workers", true), + "name": GetReadonlyNameDatasourceSchema(), + "is_disabled": datasourceSchema.BoolAttribute{ + Computed: true, + }, + "machine_policy_id": datasourceSchema.StringAttribute{ + Computed: true, + }, + "worker_pool_ids": datasourceSchema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "communication_style": datasourceSchema.StringAttribute{ + Computed: true, + }, + "health_status": datasourceSchema.StringAttribute{ + Computed: true, + }, + "proxy_id": datasourceSchema.StringAttribute{ + Computed: true, + }, + "uri": datasourceSchema.StringAttribute{ + Computed: true, + }, + "thumbprint": datasourceSchema.StringAttribute{ + Computed: true, + }, + "account_id": datasourceSchema.StringAttribute{ + Computed: true, + }, + "host": datasourceSchema.StringAttribute{ + Computed: true, + }, + "port": datasourceSchema.Int64Attribute{ + Computed: true, + }, + "fingerprint": datasourceSchema.StringAttribute{ + Computed: true, + }, + "dotnet_platform": datasourceSchema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + } +} + +type WorkersDataSourceModel struct { + ID types.String `tfsdk:"id"` + IDs types.List `tfsdk:"ids"` + Name types.String `tfsdk:"name"` + PartialName types.String `tfsdk:"partial_name"` + Skip types.Int64 `tfsdk:"skip"` + Take types.Int64 `tfsdk:"take"` + SpaceID types.String `tfsdk:"space_id"` + CommunicationStyle types.List `tfsdk:"communication_styles"` + HealthStatuses types.List `tfsdk:"health_statuses"` + IsDisabled types.Bool `tfsdk:"is_disabled"` + Workers types.List `tfsdk:"workers"` +} + +func FlattenWorker(worker *machines.Worker) attr.Value { + proxyId := types.StringNull() + uri := types.StringNull() + thumbprint := types.StringNull() + accountId := types.StringNull() + host := types.StringNull() + port := types.Int64Null() + fingerprint := types.StringNull() + dotnetPlatform := types.StringNull() + + switch endpoint := worker.Endpoint.(type) { + case *machines.ListeningTentacleEndpoint: + proxyId = util.StringOrNull(endpoint.ProxyID) + uri = util.StringOrNull(endpoint.URI.String()) + thumbprint = util.StringOrNull(endpoint.Thumbprint) + case *machines.SSHEndpoint: + proxyId = util.StringOrNull(endpoint.ProxyID) + accountId = util.StringOrNull(endpoint.AccountID) + host = util.StringOrNull(endpoint.Host) + port = types.Int64Value(int64(endpoint.Port)) + fingerprint = util.StringOrNull(endpoint.Fingerprint) + dotnetPlatform = util.StringOrNull(endpoint.DotNetCorePlatform) + } + + return types.ObjectValueMust(WorkerObjectType(), map[string]attr.Value{ + "id": types.StringValue(worker.GetID()), + "space_id": types.StringValue(worker.SpaceID), + "name": types.StringValue(worker.Name), + "is_disabled": types.BoolValue(worker.IsDisabled), + "communication_style": types.StringValue(worker.Endpoint.GetCommunicationStyle()), + "health_status": types.StringValue(worker.HealthStatus), + "machine_policy_id": types.StringValue(worker.MachinePolicyID), + "worker_pool_ids": types.ListValueMust(types.StringType, util.ToValueSlice(worker.WorkerPoolIDs)), + "proxy_id": proxyId, + "uri": uri, + "thumbprint": thumbprint, + "account_id": accountId, + "host": host, + "port": port, + "fingerprint": fingerprint, + "dotnet_platform": dotnetPlatform, + }) +} + +func WorkerObjectType() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "space_id": types.StringType, + "name": types.StringType, + "is_disabled": types.BoolType, + "communication_style": types.StringType, + "health_status": types.StringType, + "machine_policy_id": types.StringType, + "worker_pool_ids": types.ListType{ElemType: types.StringType}, + "proxy_id": types.StringType, + "uri": types.StringType, + "thumbprint": types.StringType, + "account_id": types.StringType, + "host": types.StringType, + "port": types.Int64Type, + "fingerprint": types.StringType, + "dotnet_platform": types.StringType, + } +}