From 2a22ed83c33f594dd9611ee32e948cb5fbd1f530 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 4 Nov 2024 21:30:16 +0100 Subject: [PATCH] feat: add basic implementation of tag_definitions data source --- CHANGELOG.md | 3 +- client/client.go | 2 + client/tag_definition.go | 165 ++++++++++++++ docs/data-sources/tag_definitions.md | 116 ++++++++++ .../meshstack_tag_definitions/data-source.tf | 3 + flake.nix | 7 + internal/provider/provider.go | 1 + .../provider/tag_definitions_data_source.go | 211 ++++++++++++++++++ 8 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 client/tag_definition.go create mode 100644 docs/data-sources/tag_definitions.md create mode 100644 examples/data-sources/meshstack_tag_definitions/data-source.tf create mode 100644 internal/provider/tag_definitions_data_source.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b76e247..df2bdd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ -## 0.1.0 (Unreleased) +## 0.6.0 (Unreleased) FEATURES: +- Added `meshstack_tag_definitions` data source diff --git a/client/client.go b/client/client.go index 281d040..bf0ee23 100644 --- a/client/client.go +++ b/client/client.go @@ -38,6 +38,7 @@ type endpoints struct { ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` Tenants *url.URL `json:"meshtenants"` + TagDefinitions *url.URL `json:"meshtagdefinitions"` } type loginResponse struct { @@ -63,6 +64,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), + TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), } return client, nil diff --git a/client/tag_definition.go b/client/tag_definition.go new file mode 100644 index 0000000..c82f627 --- /dev/null +++ b/client/tag_definition.go @@ -0,0 +1,165 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_TAG_DEFINITION = "application/vnd.meshcloud.api.meshtagdefinition.v1.hal+json" + +type MeshTagDefinition struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshTagDefinitionMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTagDefinitionSpec `json:"spec" tfsdk:"spec"` +} + +type MeshTagDefinitionMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshTagDefinitionSpec struct { + TargetKind string `json:"targetKind" tfsdk:"target_kind"` + Key string `json:"key" tfsdk:"key"` + ValueType MeshTagDefinitionValueType `json:"valueType" tfsdk:"value_type"` + Description string `json:"description" tfsdk:"description"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + SortOrder int64 `json:"sortOrder" tfsdk:"sort_order"` + Mandatory bool `json:"mandatory" tfsdk:"mandatory"` + Immutable bool `json:"immutable" tfsdk:"immutable"` + Restricted bool `json:"restricted" tfsdk:"restricted"` +} + +type MeshTagDefinitionValueType struct { + String *TagValueString `json:"string,omitempty" tfsdk:"string"` + Email *TagValueEmail `json:"email,omitempty" tfsdk:"email"` + Integer *TagValueInteger `json:"integer,omitempty" tfsdk:"integer"` + Number *TagValueNumber `json:"number,omitempty" tfsdk:"number"` + SingleSelect *TagValueSingleSelect `json:"singleSelect,omitempty" tfsdk:"single_select"` + MultiSelect *TagValueMultiSelect `json:"multiSelect,omitempty" tfsdk:"multi_select"` +} + +type TagValueString struct { + DefaultValue string `json:"defaultValue,omitempty" tfsdk:"default_value"` + ValidationRegex string `json:"validationRegex,omitempty" tfsdk:"validation_regex"` +} + +type TagValueEmail struct { + DefaultValue string `json:"defaultValue,omitempty" tfsdk:"default_value"` + ValidationRegex string `json:"validationRegex,omitempty" tfsdk:"validation_regex"` +} + +type TagValueInteger struct { + DefaultValue int64 `json:"defaultValue,omitempty" tfsdk:"default_value"` +} + +type TagValueNumber struct { + DefaultValue float64 `json:"defaultValue,omitempty" tfsdk:"default_value"` +} + +type TagValueSingleSelect struct { + Options []string `json:"options,omitempty" tfsdk:"options"` + DefaultValue string `json:"defaultValue,omitempty" tfsdk:"default_value"` +} + +type TagValueMultiSelect struct { + Options []string `json:"options,omitempty" tfsdk:"options"` + DefaultValue []string `json:"defaultValue,omitempty" tfsdk:"default_value"` +} + +func (c *MeshStackProviderClient) urlForTagDefinition(name string) *url.URL { + return c.endpoints.TagDefinitions.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, error) { + var all []MeshTagDefinition + + pageNumber := 0 + targetUrl := c.endpoints.TagDefinitions + query := targetUrl.Query() + + for { + query.Set("page", fmt.Sprintf("%d", pageNumber)) + + targetUrl.RawQuery = query.Encode() + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var response struct { + Embedded struct { + MeshTagDefinitions []MeshTagDefinition `json:"meshTagDefinitions"` + } `json:"_embedded"` + Page struct { + Size int `json:"size"` + TotalElements int `json:"totalElements"` + TotalPages int `json:"totalPages"` + Number int `json:"number"` + } `json:"page"` + } + + err = json.Unmarshal(data, &response) + if err != nil { + return nil, err + } + + all = append(all, response.Embedded.MeshTagDefinitions...) + + // Check if there are more pages + if response.Page.Number >= response.Page.TotalPages-1 { + break + } + + pageNumber++ + } + + return &all, nil +} + +func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefinition, error) { + targetUrl := c.urlForTagDefinition(name) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to read tag definition: %s", resp.Status) + } + + var tagDefinition MeshTagDefinition + if err := json.NewDecoder(resp.Body).Decode(&tagDefinition); err != nil { + return nil, err + } + + return &tagDefinition, nil +} diff --git a/docs/data-sources/tag_definitions.md b/docs/data-sources/tag_definitions.md new file mode 100644 index 0000000..3b92dc4 --- /dev/null +++ b/docs/data-sources/tag_definitions.md @@ -0,0 +1,116 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_tag_definitions Data Source - terraform-provider-meshstack" +subcategory: "" +description: |- + Tag definitions for the entire organization. +--- + +# meshstack_tag_definitions (Data Source) + +Tag definitions for the entire organization. + + + + +## Schema + +### Read-Only + +- `tag_definitions` (Attributes List) List of tag definitions (see [below for nested schema](#nestedatt--tag_definitions)) + + +### Nested Schema for `tag_definitions` + +Read-Only: + +- `api_version` (String) API Version of meshTagDefinition datatype. Matches the version part provided within the Accept request header. +- `kind` (String) As a common meshObject structure exists, every meshObject has a kind. This is always meshTagDefinition for this endpoint. +- `metadata` (Attributes) Always contains the 'name' to uniquely identify the meshTagDefinition. (see [below for nested schema](#nestedatt--tag_definitions--metadata)) +- `spec` (Attributes) Specification for the meshTagDefinition. (see [below for nested schema](#nestedatt--tag_definitions--spec)) + + +### Nested Schema for `tag_definitions.metadata` + +Read-Only: + +- `name` (String) Must be of the form $targetKind.$key since tag definitions must be non-conflicting. + + + +### Nested Schema for `tag_definitions.spec` + +Read-Only: + +- `description` (String) The detailed description of the tag. +- `display_name` (String) The display name of the tag. +- `immutable` (Boolean) Indicates whether the tag value is not editable after initially set. +- `key` (String) The key of the tag. +- `mandatory` (Boolean) Indicates whether the tag is mandatory. +- `restricted` (Boolean) Indicates whether only admins can edit this tag. +- `sort_order` (Number) The sort order for this tag when displayed in the UI. meshPanel sorts tags in ascending order. +- `target_kind` (String) The kind of meshObject this tag is defined for. Must be one of: meshProject, meshWorkspace, meshLandingZone, meshPaymentMethod or meshBuildingBlockDefinition. +- `value_type` (Attributes) The TagValueType of the tag. Must define exactly one of the available types. (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type)) + + +### Nested Schema for `tag_definitions.spec.value_type` + +Read-Only: + +- `email` (Attributes) email address, represented as JSON string (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type--email)) +- `integer` (Attributes) an integer, represented as a JSON number (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type--integer)) +- `multi_select` (Attributes) one or multiple strings from a list of options, represented as a JSON array (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type--multi_select)) +- `number` (Attributes) a decimal number, represented as a JSON number (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type--number)) +- `single_select` (Attributes) a string from a list of options, represented as a JSON string (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type--single_select)) +- `string` (Attributes) string, represented as JSON string (see [below for nested schema](#nestedatt--tag_definitions--spec--value_type--string)) + + +### Nested Schema for `tag_definitions.spec.value_type.string` + +Read-Only: + +- `default_value` (String) The default value of the tag. +- `validation_regex` (String) The regex pattern that the tag value must match. + + + +### Nested Schema for `tag_definitions.spec.value_type.string` + +Read-Only: + +- `default_value` (Number) The default value of the tag. + + + +### Nested Schema for `tag_definitions.spec.value_type.string` + +Read-Only: + +- `default_value` (List of String) The default value of the tag. +- `options` (List of String) The allowed options for the tag as a string[] + + + +### Nested Schema for `tag_definitions.spec.value_type.string` + +Read-Only: + +- `default_value` (Number) The default value of the tag. + + + +### Nested Schema for `tag_definitions.spec.value_type.string` + +Read-Only: + +- `default_value` (String) The default value of the tag. +- `options` (List of String) The allowed options for the tag as a string[] + + + +### Nested Schema for `tag_definitions.spec.value_type.string` + +Read-Only: + +- `default_value` (String) The default value of the tag. +- `validation_regex` (String) The regex pattern that the tag value must match. diff --git a/examples/data-sources/meshstack_tag_definitions/data-source.tf b/examples/data-sources/meshstack_tag_definitions/data-source.tf new file mode 100644 index 0000000..fed55fd --- /dev/null +++ b/examples/data-sources/meshstack_tag_definitions/data-source.tf @@ -0,0 +1,3 @@ +data "meshstack_tag_definitions" "all" { + # no attributes for filtering are supported at the moment +} diff --git a/flake.nix b/flake.nix index 229c820..c3f800d 100644 --- a/flake.nix +++ b/flake.nix @@ -31,6 +31,13 @@ # https://github.com/golangci/golangci-lint golangci-lint ]; + + # make tfplugindocs available in the shell, see https://github.com/hashicorp/terraform-plugin-docs?tab=readme-ov-file#installation + shellHook = '' + export GOBIN=$PWD/bin + export PATH=$GOBIN:$PATH + go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs + ''; }; }); }; diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3fccd4a..5510fba 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -92,6 +92,7 @@ func (p *MeshStackProvider) DataSources(ctx context.Context) []func() datasource NewProjectUserBindingDataSource, NewProjectGroupBindingDataSource, NewTenantDataSource, + NewTagDefinitionsDataSource, } } diff --git a/internal/provider/tag_definitions_data_source.go b/internal/provider/tag_definitions_data_source.go new file mode 100644 index 0000000..ce37917 --- /dev/null +++ b/internal/provider/tag_definitions_data_source.go @@ -0,0 +1,211 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/meshcloud/terraform-provider-meshstack/client" +) + +func NewTagDefinitionsDataSource() datasource.DataSource { + return &tagDefinitionsDataSource{} +} + +type tagDefinitionsDataSource struct { + client *client.MeshStackProviderClient +} + +func (d *tagDefinitionsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tag_definitions" +} + +func (d *tagDefinitionsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Tag definitions for the entire organization.", + + Attributes: map[string]schema.Attribute{ + "tag_definitions": schema.ListNestedAttribute{ + MarkdownDescription: "List of tag definitions", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "kind": schema.StringAttribute{ + MarkdownDescription: "As a common meshObject structure exists, every meshObject has a kind. This is always meshTagDefinition for this endpoint.", + Computed: true, + }, + "api_version": schema.StringAttribute{ + MarkdownDescription: "API Version of meshTagDefinition datatype. Matches the version part provided within the Accept request header.", + Computed: true, + }, + "metadata": schema.SingleNestedAttribute{ + MarkdownDescription: "Always contains the 'name' to uniquely identify the meshTagDefinition.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Must be of the form $targetKind.$key since tag definitions must be non-conflicting.", + Computed: true, + }, + }, + }, + "spec": schema.SingleNestedAttribute{ + MarkdownDescription: "Specification for the meshTagDefinition.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "target_kind": schema.StringAttribute{ + MarkdownDescription: "The kind of meshObject this tag is defined for. Must be one of: meshProject, meshWorkspace, meshLandingZone, meshPaymentMethod or meshBuildingBlockDefinition.", + Computed: true, + }, + "key": schema.StringAttribute{ + MarkdownDescription: "The key of the tag.", + Computed: true, + }, + "value_type": schema.SingleNestedAttribute{ + MarkdownDescription: "The TagValueType of the tag. Must define exactly one of the available types.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "string": schema.SingleNestedAttribute{ + MarkdownDescription: "string, represented as JSON string", + Computed: true, + Attributes: map[string]schema.Attribute{ + "default_value": schema.StringAttribute{ + MarkdownDescription: "The default value of the tag.", + Computed: true, + }, + "validation_regex": schema.StringAttribute{ + MarkdownDescription: "The regex pattern that the tag value must match.", + Computed: true, + }, + }, + }, + "email": schema.SingleNestedAttribute{ + MarkdownDescription: "email address, represented as JSON string", + Computed: true, + Attributes: map[string]schema.Attribute{ + "default_value": schema.StringAttribute{ + MarkdownDescription: "The default value of the tag.", + Computed: true, + }, + "validation_regex": schema.StringAttribute{ + MarkdownDescription: "The regex pattern that the tag value must match.", + Computed: true, + }, + }, + }, + "integer": schema.SingleNestedAttribute{ + MarkdownDescription: "an integer, represented as a JSON number", + Computed: true, + Attributes: map[string]schema.Attribute{ + "default_value": schema.Int64Attribute{ + MarkdownDescription: "The default value of the tag.", + Computed: true, + }, + }, + }, + "number": schema.SingleNestedAttribute{ + MarkdownDescription: "a decimal number, represented as a JSON number", + Computed: true, + Attributes: map[string]schema.Attribute{ + "default_value": schema.Float64Attribute{ + MarkdownDescription: "The default value of the tag.", + Computed: true, + }, + }, + }, + "single_select": schema.SingleNestedAttribute{ + MarkdownDescription: "a string from a list of options, represented as a JSON string", + Computed: true, + Attributes: map[string]schema.Attribute{ + "options": schema.ListAttribute{ + MarkdownDescription: "The allowed options for the tag as a string[]", + Computed: true, + ElementType: types.StringType, + }, + "default_value": schema.StringAttribute{ + MarkdownDescription: "The default value of the tag.", + Computed: true, + }, + }, + }, + "multi_select": schema.SingleNestedAttribute{ + MarkdownDescription: "one or multiple strings from a list of options, represented as a JSON array", + Computed: true, + Attributes: map[string]schema.Attribute{ + "options": schema.ListAttribute{ + MarkdownDescription: "The allowed options for the tag as a string[]", + Computed: true, + ElementType: types.StringType, + }, + "default_value": schema.ListAttribute{ + MarkdownDescription: "The default value of the tag.", + Computed: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "The detailed description of the tag.", + Computed: true, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The display name of the tag.", + Computed: true, + }, + "sort_order": schema.Int64Attribute{ + MarkdownDescription: "The sort order for this tag when displayed in the UI. meshPanel sorts tags in ascending order.", + Computed: true, + }, + "mandatory": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether the tag is mandatory.", + Computed: true, + }, + "immutable": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether the tag value is not editable after initially set.", + Computed: true, + }, + "restricted": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether only admins can edit this tag.", + Computed: true, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (d *tagDefinitionsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.MeshStackProviderClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *tagDefinitionsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tags, err := d.client.ReadTagDefinitions() + if err != nil { + resp.Diagnostics.AddError("Unable to read meshTagDefinitions", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("tag_definitions"), &tags)...) +}