From 07964864f2f3d64fb63382e83d352cb6ecd935c6 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 19 Jun 2024 13:28:13 +0200 Subject: [PATCH 001/200] refactor: move client to separate package --- buildingblock.go | 90 +++++++++++++++ client.go | 216 ++++++++++++++++++++++++++++++++++++ project.go | 236 ++++++++++++++++++++++++++++++++++++++++ project_user_binding.go | 120 ++++++++++++++++++++ tenant.go | 138 +++++++++++++++++++++++ 5 files changed, 800 insertions(+) create mode 100644 buildingblock.go create mode 100644 client.go create mode 100644 project.go create mode 100644 project_user_binding.go create mode 100644 tenant.go diff --git a/buildingblock.go b/buildingblock.go new file mode 100644 index 0000000..5580e7b --- /dev/null +++ b/buildingblock.go @@ -0,0 +1,90 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +type MeshBuildingBlock struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshBuildingBlockMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` + Status MeshBuildingBlockStatus `json:"status" tfsdk:"status"` +} + +type MeshBuildingBlockMetadata struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` + DefinitionVersion int64 `json:"definitionVersion" tfsdk:"definition_version"` + TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` + MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` +} + +type MeshBuildingBlockSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` + ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` +} + +type MeshBuildingBlockIO struct { + Key string `json:"key" tfsdk:"key"` + Value interface{} `json:"value" tfsdk:"value"` + ValueType string `json:"valueType" tfsdk:"value_type"` +} + +type MeshBuildingBlockParent struct { + BuildingBlockUuid string `json:"buildingBlockUuid" tfsdk:"buildingblock_uuid"` + DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` +} + +type MeshBuildingBlockStatus struct { + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` +} + +func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { + if c.ensureValidToken() != nil { + return nil, errors.New(ERROR_AUTHENTICATION_FAILURE) + } + + targetUrl := c.endpoints.BuildingBlocks.JoinPath(uuid) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var bb MeshBuildingBlock + err = json.Unmarshal(data, &bb) + if err != nil { + return nil, err + } + + return &bb, nil +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..9158f69 --- /dev/null +++ b/client.go @@ -0,0 +1,216 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + apiMeshObjectsRoot = "/api/meshobjects" + loginEndpoint = "/api/login" + + ERROR_GENERIC_CLIENT_ERROR = "client error" + ERROR_GENERIC_API_ERROR = "api error" + ERROR_AUTHENTICATION_FAILURE = "Not authorized. Check api key and secret." + ERROR_ENDPOINT_LOOKUP = "Could not fetch endpoints for meshStack." + + CONTENT_TYPE_PROJECT = "application/vnd.meshcloud.api.meshproject.v2.hal+json" + CONTENT_TYPE_TENANT = "application/vnd.meshcloud.api.meshtenant.v3.hal+json" + CONTENT_TYPE_PROJECT_USER_BINDINGS = "application/vnd.meshcloud.api.meshprojectuserbinding.v1.hal+json" + CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshprojectuserbinding.v3.hal+json" +) + +type MeshStackProviderClient struct { + url *url.URL + httpClient *http.Client + apiKey string + apiSecret string + token string + tokenExpiry time.Time + endpoints endpoints +} + +type endpoints struct { + BuildingBlocks *url.URL `json:"meshbuildingblocks"` + Projects *url.URL `json:"meshprojects"` + ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` + Tenants *url.URL `json:"meshtenants"` +} + +type loginResponse struct { + Token string `json:"access_token"` + ExpireSec int `json:"expires_in"` +} + +func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackProviderClient, error) { + client := &MeshStackProviderClient{ + url: rootUrl, + httpClient: &http.Client{ + Timeout: time.Minute * 5, + }, + apiKey: apiKey, + apiSecret: apiSecret, + token: "", + } + + // TODO: lookup endpoints + client.endpoints = endpoints{ + BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), + Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), + ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), + Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), + } + + return client, nil +} + +func (c *MeshStackProviderClient) login() error { + loginPath, err := url.JoinPath(c.url.String(), loginEndpoint) + if err != nil { + return err + } + + formData := url.Values{} + formData.Set("client_id", c.apiKey) + formData.Set("client_secret", c.apiSecret) + formData.Set("grant_type", "client_credentials") + + req, _ := http.NewRequest(http.MethodPost, loginPath, strings.NewReader(formData.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err := c.httpClient.Do(req) + + if err != nil || res.StatusCode != 200 { + return errors.New(ERROR_AUTHENTICATION_FAILURE) + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + var loginResult loginResponse + err = json.Unmarshal(data, &loginResult) + if err != nil { + return err + } + + c.token = fmt.Sprintf("Bearer %s", loginResult.Token) + c.tokenExpiry = time.Now().Add(time.Second * time.Duration(loginResult.ExpireSec)) + + return nil +} + +func (c *MeshStackProviderClient) ensureValidToken() error { + if c.token == "" || time.Now().Add(time.Second*30).After(c.tokenExpiry) { + return c.login() + } + return nil +} + +// nolint: unused +func (c *MeshStackProviderClient) lookUpEndpoints() error { + if c.ensureValidToken() != nil { + return errors.New(ERROR_AUTHENTICATION_FAILURE) + } + + meshObjectsPath, err := url.JoinPath(c.url.String(), apiMeshObjectsRoot) + if err != nil { + return err + } + meshObjects, _ := url.Parse(meshObjectsPath) + + res, err := c.httpClient.Do( + &http.Request{ + URL: meshObjects, + Method: "GET", + Header: http.Header{ + "Authorization": {c.token}, + }, + }, + ) + + if err != nil { + return errors.New(ERROR_GENERIC_CLIENT_ERROR) + } + + defer res.Body.Close() + + if res.StatusCode != 200 { + return errors.New(ERROR_AUTHENTICATION_FAILURE) + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + var endpoints endpoints + err = json.Unmarshal(data, &endpoints) + if err != nil { + return err + } + + c.endpoints = endpoints + return nil +} + +func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request) (*http.Response, error) { + // ensure that headeres are initialized + if req.Header == nil { + req.Header = map[string][]string{} + } + req.Header.Set("User-Agent", "meshStack Terraform Provider") + + // log request before adding auth + log.Println(req) + + // add authentication + if c.ensureValidToken() != nil { + return nil, errors.New(ERROR_AUTHENTICATION_FAILURE) + } + req.Header.Set("Authorization", c.token) + + res, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + log.Println(res) + + return res, nil +} + +func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedStatus int) error { + req, err := http.NewRequest("DELETE", targetUrl.String(), nil) + if err != nil { + return err + } + + res, err := c.doAuthenticatedRequest(req) + + if err != nil { + return errors.New(ERROR_GENERIC_CLIENT_ERROR) + } + + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if res.StatusCode != expectedStatus { + return fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + return nil +} diff --git a/project.go b/project.go new file mode 100644 index 0000000..f9d97f8 --- /dev/null +++ b/project.go @@ -0,0 +1,236 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type MeshProject struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshProjectMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshProjectSpec `json:"spec" tfsdk:"spec"` +} + +type MeshProjectMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` +} + +type MeshProjectSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` + PaymentMethodIdentifier *string `json:"paymentMethodIdentifier" tfsdk:"payment_method_identifier"` + SubstitutePaymentMethodIdentifier *string `json:"substitutePaymentMethodIdentifier" tfsdk:"substitute_payment_method_identifier"` +} + +type MeshProjectCreate struct { + Metadata MeshProjectCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshProjectSpec `json:"spec" tfsdk:"spec"` +} + +type MeshProjectCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +func (c *MeshStackProviderClient) urlForProject(workspace string, name string) *url.URL { + identifier := workspace + "." + name + return c.endpoints.Projects.JoinPath(identifier) +} + +func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*MeshProject, error) { + targetUrl := c.urlForProject(workspace, name) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_PROJECT) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var project MeshProject + err = json.Unmarshal(data, &project) + if err != nil { + return nil, err + } + + return &project, nil +} + +func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, paymentMethodIdentifier *string) (*[]MeshProject, error) { + var allProjects []MeshProject + + pageNumber := 0 + targetUrl := c.endpoints.Projects + query := targetUrl.Query() + query.Set("workspaceIdentifier", workspaceIdentifier) + if paymentMethodIdentifier != nil { + query.Set("paymentIdentifier", *paymentMethodIdentifier) + } + + 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_PROJECT) + + 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 { + MeshProjects []MeshProject `json:"meshProjects"` + } `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 + } + + allProjects = append(allProjects, response.Embedded.MeshProjects...) + + // Check if there are more pages + if response.Page.Number >= response.Page.TotalPages-1 { + break + } + + pageNumber++ + } + + return &allProjects, nil +} + +func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*MeshProject, error) { + payload, err := json.Marshal(project) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Projects.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) + req.Header.Set("Accept", CONTENT_TYPE_PROJECT) + + 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, err + } + + if res.StatusCode != 201 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdProject MeshProject + err = json.Unmarshal(data, &createdProject) + if err != nil { + return nil, err + } + + return &createdProject, nil +} + +func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*MeshProject, error) { + targetUrl := c.urlForProject(project.Metadata.OwnedByWorkspace, project.Metadata.Name) + + payload, err := json.Marshal(project) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) + req.Header.Set("Accept", CONTENT_TYPE_PROJECT) + + 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, err + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var updatedProject MeshProject + err = json.Unmarshal(data, &updatedProject) + if err != nil { + return nil, err + } + + return &updatedProject, nil +} + +func (c *MeshStackProviderClient) DeleteProject(workspace string, name string) error { + targetUrl := c.urlForProject(workspace, name) + return c.deleteMeshObject(*targetUrl, 202) +} diff --git a/project_user_binding.go b/project_user_binding.go new file mode 100644 index 0000000..0c6f601 --- /dev/null +++ b/project_user_binding.go @@ -0,0 +1,120 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type MeshProjectUserBinding struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshProjectUserBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshSubject `json:"subject" tfsdk:"subject"` +} + +type MeshProjectUserBindingMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshProjectRoleRef struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshProjectTargetRef struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshSubject struct { + Name string `json:"name" tfsdk:"name"` +} + +func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL { + return c.endpoints.ProjectUserBindings.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { + targetUrl := c.urlForPojectUserBinding(name) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_PROJECT_USER_BINDING) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var binding MeshProjectUserBinding + err = json.Unmarshal(data, &binding) + if err != nil { + return nil, err + } + + return &binding, nil +} + +func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { + payload, err := json.Marshal(binding) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.ProjectUserBindings.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT_USER_BINDING) + req.Header.Set("Accept", CONTENT_TYPE_PROJECT_USER_BINDING) + + 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, err + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBinding MeshProjectUserBinding + err = json.Unmarshal(data, &createdBinding) + if err != nil { + return nil, err + } + + return &createdBinding, nil +} + +func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { + targetUrl := c.urlForPojectUserBinding(name) + return c.deleteMeshObject(*targetUrl, 204) +} diff --git a/tenant.go b/tenant.go new file mode 100644 index 0000000..b8026aa --- /dev/null +++ b/tenant.go @@ -0,0 +1,138 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type MeshTenant struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshTenantMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantSpec `json:"spec" tfsdk:"spec"` +} + +type MeshTenantMetadata struct { + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` + AssignedTags map[string][]string `json:"assignedTags" tfsdk:"assigned_tags"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` +} + +type MeshTenantSpec struct { + LocalId *string `json:"localId" tfsdk:"local_id"` + LandingZoneIdentifier string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` + Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` +} + +type MeshTenantQuota struct { + Key string `json:"key" tfsdk:"key"` + Value int64 `json:"value" tfsdk:"value"` +} + +type MeshTenantCreate struct { + Metadata MeshTenantCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantCreateSpec `json:"spec" tfsdk:"spec"` +} + +type MeshTenantCreateMetadata struct { + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` +} + +type MeshTenantCreateSpec struct { + LocalId *string `json:"localId" tfsdk:"local_id"` + LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` + Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` +} + +func (c *MeshStackProviderClient) urlForTenant(workspace string, project string, platform string) *url.URL { + identifier := workspace + "." + project + "." + platform + return c.endpoints.Tenants.JoinPath(identifier) +} + +func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, platform string) (*MeshTenant, error) { + targetUrl := c.urlForTenant(workspace, project, platform) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_TENANT) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var tenant MeshTenant + err = json.Unmarshal(data, &tenant) + if err != nil { + return nil, err + } + + return &tenant, nil +} + +func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshTenant, error) { + payload, err := json.Marshal(tenant) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Tenants.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_TENANT) + req.Header.Set("Accept", CONTENT_TYPE_TENANT) + + 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, err + } + + if res.StatusCode != 201 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdTenant MeshTenant + err = json.Unmarshal(data, &createdTenant) + if err != nil { + return nil, err + } + + return &createdTenant, nil +} + +func (c *MeshStackProviderClient) DeleteTenant(workspace string, project string, platform string) error { + targetUrl := c.urlForTenant(workspace, project, platform) + return c.deleteMeshObject(*targetUrl, 202) +} From a0baddf16cbeaa2bf0f1380f6860af0dd71a7fc3 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 21 Jun 2024 16:10:36 +0200 Subject: [PATCH 002/200] feat: project group bindings --- client.go | 23 +++---- project.go | 2 + project_binding.go | 134 +++++++++++++++++++++++++++++++++++++++ project_group_binding.go | 26 ++++++++ project_user_binding.go | 102 ++--------------------------- tenant.go | 2 + 6 files changed, 178 insertions(+), 111 deletions(-) create mode 100644 project_binding.go create mode 100644 project_group_binding.go diff --git a/client.go b/client.go index 9158f69..281d040 100644 --- a/client.go +++ b/client.go @@ -20,11 +20,6 @@ const ( ERROR_GENERIC_API_ERROR = "api error" ERROR_AUTHENTICATION_FAILURE = "Not authorized. Check api key and secret." ERROR_ENDPOINT_LOOKUP = "Could not fetch endpoints for meshStack." - - CONTENT_TYPE_PROJECT = "application/vnd.meshcloud.api.meshproject.v2.hal+json" - CONTENT_TYPE_TENANT = "application/vnd.meshcloud.api.meshtenant.v3.hal+json" - CONTENT_TYPE_PROJECT_USER_BINDINGS = "application/vnd.meshcloud.api.meshprojectuserbinding.v1.hal+json" - CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshprojectuserbinding.v3.hal+json" ) type MeshStackProviderClient struct { @@ -38,10 +33,11 @@ type MeshStackProviderClient struct { } type endpoints struct { - BuildingBlocks *url.URL `json:"meshbuildingblocks"` - Projects *url.URL `json:"meshprojects"` - ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` - Tenants *url.URL `json:"meshtenants"` + BuildingBlocks *url.URL `json:"meshbuildingblocks"` + Projects *url.URL `json:"meshprojects"` + ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` + ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` + Tenants *url.URL `json:"meshtenants"` } type loginResponse struct { @@ -62,10 +58,11 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro // TODO: lookup endpoints client.endpoints = endpoints{ - BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), - Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), - ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), - Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), + BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), + Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), + ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), + ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), + Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), } return client, nil diff --git a/project.go b/project.go index f9d97f8..eca75ca 100644 --- a/project.go +++ b/project.go @@ -9,6 +9,8 @@ import ( "net/url" ) +const CONTENT_TYPE_PROJECT = "application/vnd.meshcloud.api.meshproject.v2.hal+json" + type MeshProject struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` diff --git a/project_binding.go b/project_binding.go new file mode 100644 index 0000000..f3b4d41 --- /dev/null +++ b/project_binding.go @@ -0,0 +1,134 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type MeshProjectBinding struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshProjectBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshSubject `json:"subject" tfsdk:"subject"` +} + +type MeshProjectBindingMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshProjectRoleRef struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshProjectTargetRef struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshSubject struct { + Name string `json:"name" tfsdk:"name"` +} + +func (c *MeshStackProviderClient) readProjectBinding(name string, contentType string) (*MeshProjectBinding, error) { + var targetUrl *url.URL + switch contentType { + case CONTENT_TYPE_PROJECT_USER_BINDING: + targetUrl = c.urlForPojectUserBinding(name) + + case CONTENT_TYPE_PROJECT_GROUP_BINDING: + targetUrl = c.urlForPojectGroupBinding(name) + + default: + return nil, fmt.Errorf("Unexpected content type: %s", contentType) + } + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var binding MeshProjectBinding + err = json.Unmarshal(data, &binding) + if err != nil { + return nil, err + } + + return &binding, nil +} + +func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBinding, contentType string) (*MeshProjectBinding, error) { + var targetUrl *url.URL + switch contentType { + case CONTENT_TYPE_PROJECT_USER_BINDING: + targetUrl = c.endpoints.ProjectUserBindings + + case CONTENT_TYPE_PROJECT_GROUP_BINDING: + targetUrl = c.endpoints.ProjectGroupBindings + + default: + return nil, fmt.Errorf("Unexpected content type: %s", contentType) + } + + payload, err := json.Marshal(binding) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT_GROUP_BINDING) + req.Header.Set("Accept", CONTENT_TYPE_PROJECT_GROUP_BINDING) + + 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, err + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBinding MeshProjectBinding + err = json.Unmarshal(data, &createdBinding) + if err != nil { + return nil, err + } + + return &createdBinding, nil +} diff --git a/project_group_binding.go b/project_group_binding.go new file mode 100644 index 0000000..8b66818 --- /dev/null +++ b/project_group_binding.go @@ -0,0 +1,26 @@ +package client + +import ( + "net/url" +) + +const CONTENT_TYPE_PROJECT_GROUP_BINDING = "application/vnd.meshcloud.api.meshprojectgroupbinding.v3.hal+json" + +type MeshProjectGroupBinding = MeshProjectBinding + +func (c *MeshStackProviderClient) urlForPojectGroupBinding(name string) *url.URL { + return c.endpoints.ProjectGroupBindings.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadProjectGroupBinding(name string) (*MeshProjectGroupBinding, error) { + return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { + return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { + targetUrl := c.urlForPojectGroupBinding(name) + return c.deleteMeshObject(*targetUrl, 204) +} diff --git a/project_user_binding.go b/project_user_binding.go index 0c6f601..3de8e22 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -1,117 +1,23 @@ package client import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" "net/url" ) -type MeshProjectUserBinding struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshProjectUserBindingMetadata `json:"metadata" tfsdk:"metadata"` - RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` - TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshSubject `json:"subject" tfsdk:"subject"` -} - -type MeshProjectUserBindingMetadata struct { - Name string `json:"name" tfsdk:"name"` -} - -type MeshProjectRoleRef struct { - Name string `json:"name" tfsdk:"name"` -} - -type MeshProjectTargetRef struct { - Name string `json:"name" tfsdk:"name"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` -} +const CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshprojectuserbinding.v3.hal+json" -type MeshSubject struct { - Name string `json:"name" tfsdk:"name"` -} +type MeshProjectUserBinding = MeshProjectBinding func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL { return c.endpoints.ProjectUserBindings.JoinPath(name) } func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { - targetUrl := c.urlForPojectUserBinding(name) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_PROJECT_USER_BINDING) - - 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, err - } - - if res.StatusCode == 404 { - return nil, nil - } - - if res.StatusCode != 200 { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - - var binding MeshProjectUserBinding - err = json.Unmarshal(data, &binding) - if err != nil { - return nil, err - } - - return &binding, nil + return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_USER_BINDING) } func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - payload, err := json.Marshal(binding) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.ProjectUserBindings.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT_USER_BINDING) - req.Header.Set("Accept", CONTENT_TYPE_PROJECT_USER_BINDING) - - 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, err - } - - if res.StatusCode != 200 { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - - var createdBinding MeshProjectUserBinding - err = json.Unmarshal(data, &createdBinding) - if err != nil { - return nil, err - } - - return &createdBinding, nil + return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_USER_BINDING) } func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { diff --git a/tenant.go b/tenant.go index b8026aa..b83c5e6 100644 --- a/tenant.go +++ b/tenant.go @@ -9,6 +9,8 @@ import ( "net/url" ) +const CONTENT_TYPE_TENANT = "application/vnd.meshcloud.api.meshtenant.v3.hal+json" + type MeshTenant struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` From aa3165b031974ac9d17d5109aaa9af9cd6ae7941 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 28 Jun 2024 15:28:43 +0200 Subject: [PATCH 003/200] feat: building block resource --- buildingblock.go | 80 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index 5580e7b..8b8a7d6 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -1,11 +1,23 @@ package client import ( + "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" + "net/url" +) + +const ( + MESH_BUILDING_BLOCK_IO_TYPE_STRING = "STRING" + MESH_BUILDING_BLOCK_IO_TYPE_INTEGER = "INTEGER" + MESH_BUILDING_BLOCK_IO_TYPE_BOOLEAN = "BOOLEAN" + MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT = "SINGLE_SELECT" + MESH_BUILDING_BLOCK_IO_TYPE_FILE = "FILE" + MESH_BUILDING_BLOCK_IO_TYPE_LIST = "LIST" + + CONTENT_TYPE_BUILDING_BLOCK = "application/vnd.meshcloud.api.meshbuildingblock.v1.hal+json" ) type MeshBuildingBlock struct { @@ -49,12 +61,25 @@ type MeshBuildingBlockStatus struct { Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` } -func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { - if c.ensureValidToken() != nil { - return nil, errors.New(ERROR_AUTHENTICATION_FAILURE) - } +type MeshBuildingBlockCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshBuildingBlockCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` +} + +type MeshBuildingBlockCreateMetadata struct { + DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` + DefinitionVersion int64 `json:"definitionVersion" tfsdk:"definition_version"` + TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` +} + +func (c *MeshStackProviderClient) urlForBuildingBlock(uuid string) *url.URL { + return c.endpoints.BuildingBlocks.JoinPath(uuid) +} - targetUrl := c.endpoints.BuildingBlocks.JoinPath(uuid) +func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { + targetUrl := c.urlForBuildingBlock(uuid) req, err := http.NewRequest("GET", targetUrl.String(), nil) if err != nil { return nil, err @@ -88,3 +113,46 @@ func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingB return &bb, nil } + +func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { + payload, err := json.Marshal(bb) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.BuildingBlocks.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK) + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) + + 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, err + } + + if res.StatusCode != 201 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBb MeshBuildingBlock + err = json.Unmarshal(data, &createdBb) + if err != nil { + return nil, err + } + + return &createdBb, nil +} + +func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { + targetUrl := c.urlForBuildingBlock(uuid) + return c.deleteMeshObject(*targetUrl, 202) +} From 0947438b93a0610f9d8a4717ec9f79b3956501fc Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 4 Jul 2024 11:38:42 +0200 Subject: [PATCH 004/200] fix: updated status codes --- buildingblock.go | 2 +- project.go | 2 +- tenant.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index 8b8a7d6..44d1e02 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -139,7 +139,7 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat return nil, err } - if res.StatusCode != 201 { + if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/project.go b/project.go index eca75ca..9dde431 100644 --- a/project.go +++ b/project.go @@ -178,7 +178,7 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me return nil, err } - if res.StatusCode != 201 { + if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/tenant.go b/tenant.go index b83c5e6..9a4775a 100644 --- a/tenant.go +++ b/tenant.go @@ -121,7 +121,7 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT return nil, err } - if res.StatusCode != 201 { + if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From 7cdf4b7eed1d4d52a20d37136c1d184e4f66e740 Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Tue, 9 Jul 2024 14:02:15 +0200 Subject: [PATCH 005/200] fix: contentType for project user binding --- project_binding.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_binding.go b/project_binding.go index f3b4d41..fdb71e5 100644 --- a/project_binding.go +++ b/project_binding.go @@ -105,8 +105,8 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi if err != nil { return nil, err } - req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT_GROUP_BINDING) - req.Header.Set("Accept", CONTENT_TYPE_PROJECT_GROUP_BINDING) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept", contentType) res, err := c.doAuthenticatedRequest(req) if err != nil { From af11b1d40a8f98ec7480c50b89a1d82ffddc1a43 Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Fri, 2 Aug 2024 12:30:47 +0200 Subject: [PATCH 006/200] fix: Set Accept header when getting a Building Block meshStack enforces the Accept header soon, so we have to make sure to always provide it --- buildingblock.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buildingblock.go b/buildingblock.go index 44d1e02..b31c2bf 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -80,10 +80,12 @@ func (c *MeshStackProviderClient) urlForBuildingBlock(uuid string) *url.URL { func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { targetUrl := c.urlForBuildingBlock(uuid) + req, err := http.NewRequest("GET", targetUrl.String(), nil) if err != nil { return nil, err } + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) res, err := c.doAuthenticatedRequest(req) if err != nil { From 4b23a7e18fe6a50ad16cf552777f9678eeb11b7e Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 14 Nov 2024 15:03:14 +0100 Subject: [PATCH 007/200] fix: http response code for building block creation is now 201 --- buildingblock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildingblock.go b/buildingblock.go index b31c2bf..c9c0358 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -141,7 +141,7 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat return nil, err } - if res.StatusCode != 200 { + if res.StatusCode != 201 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From f9c7a9ef4e8ad1f26dc83fba354177331297eaf5 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 15 Nov 2024 09:53:44 +0100 Subject: [PATCH 008/200] fix: response codes for project and tenant creation --- project.go | 2 +- tenant.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project.go b/project.go index 9dde431..eca75ca 100644 --- a/project.go +++ b/project.go @@ -178,7 +178,7 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me return nil, err } - if res.StatusCode != 200 { + if res.StatusCode != 201 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/tenant.go b/tenant.go index 9a4775a..b83c5e6 100644 --- a/tenant.go +++ b/tenant.go @@ -121,7 +121,7 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT return nil, err } - if res.StatusCode != 200 { + if res.StatusCode != 201 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From 4d96b33d237c268338be09fb528de4a3fc9631f1 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 4 Nov 2024 21:30:16 +0100 Subject: [PATCH 009/200] feat: add basic implementation of tag_definitions data source --- client.go | 2 + tag_definition.go | 165 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 tag_definition.go diff --git a/client.go b/client.go index 281d040..bf0ee23 100644 --- a/client.go +++ b/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/tag_definition.go b/tag_definition.go new file mode 100644 index 0000000..c82f627 --- /dev/null +++ b/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 +} From 656d18bb04a635fc8e5fc0628eb1b6c03c7805b5 Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Tue, 5 Nov 2024 16:24:07 +0100 Subject: [PATCH 010/200] feat: meshstack_tag_definition data source (GET) --- tag_definition.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tag_definition.go b/tag_definition.go index c82f627..b1182d9 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -146,7 +146,9 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini return nil, err } - resp, err := c.httpClient.Do(req) + req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) + + resp, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } From 28c9595802803f96742e3a03464db67fc654f8a8 Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Wed, 6 Nov 2024 09:24:11 +0100 Subject: [PATCH 011/200] feat: meshstack_tag_definition resource --- tag_definition.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tag_definition.go b/tag_definition.go index b1182d9..b73ac6a 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "encoding/json" "fmt" "io" @@ -165,3 +166,93 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini return &tagDefinition, nil } + +func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { + targetUrl := c.endpoints.TagDefinitions + data, err := json.Marshal(tagDefinition) + if err != nil { + return nil, fmt.Errorf("failed to marshal tag definition: %w", err) + } + + fmt.Printf("JSON Payload: %s\n", string(data)) + + req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) + req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) + + resp, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, fmt.Errorf("failed to do authenticated request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("failed to create tag definition: %s", resp.Status) + } + + var createdTagDefinition MeshTagDefinition + if err := json.NewDecoder(resp.Body).Decode(&createdTagDefinition); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &createdTagDefinition, nil +} + +func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { + targetUrl := c.urlForTagDefinition(tagDefinition.Metadata.Name) + data, err := json.Marshal(tagDefinition) + if err != nil { + return nil, fmt.Errorf("failed to marshal tag definition: %w", err) + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) + req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) + + resp, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, fmt.Errorf("failed to do authenticated request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to update tag definition: %s", resp.Status) + } + + var updatedTagDefinition MeshTagDefinition + if err := json.NewDecoder(resp.Body).Decode(&updatedTagDefinition); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &updatedTagDefinition, nil +} + +func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { + targetUrl := c.urlForTagDefinition(name) + req, err := http.NewRequest("DELETE", targetUrl.String(), nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) + + resp, err := c.doAuthenticatedRequest(req) + if err != nil { + return fmt.Errorf("failed to do authenticated request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed to delete tag definition: %s", resp.Status) + } + + return nil +} From 94e7d03faeb10133fad75948149ac2e8ae669c99 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Tue, 12 Nov 2024 13:53:08 +0100 Subject: [PATCH 012/200] feat: set metadata.name automatically for tag_definition --- tag_definition.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tag_definition.go b/tag_definition.go index b73ac6a..1855b26 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -9,6 +9,7 @@ import ( "net/url" ) +const API_VERSION_TAG_DEFINITION = "v1" const CONTENT_TYPE_TAG_DEFINITION = "application/vnd.meshcloud.api.meshtagdefinition.v1.hal+json" type MeshTagDefinition struct { From 76f7c6c67e888829d64fd3499bbcff1b43e0c690 Mon Sep 17 00:00:00 2001 From: Le Date: Tue, 10 Dec 2024 19:43:26 +0100 Subject: [PATCH 013/200] refactor: check success by 2xx range --- buildingblock.go | 4 ++-- project.go | 8 ++++---- project_binding.go | 4 ++-- status_code_checker.go | 13 +++++++++++++ tag_definition.go | 8 ++++---- tenant.go | 4 ++-- 6 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 status_code_checker.go diff --git a/buildingblock.go b/buildingblock.go index c9c0358..41c4252 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -103,7 +103,7 @@ func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingB return nil, nil } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -141,7 +141,7 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat return nil, err } - if res.StatusCode != 201 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/project.go b/project.go index eca75ca..683b2ee 100644 --- a/project.go +++ b/project.go @@ -71,7 +71,7 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M return nil, nil } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -119,7 +119,7 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme return nil, fmt.Errorf("failed to read response body: %w", err) } - if res.StatusCode != http.StatusOK { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -178,7 +178,7 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me return nil, err } - if res.StatusCode != 201 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -219,7 +219,7 @@ func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*Me return nil, err } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/project_binding.go b/project_binding.go index fdb71e5..f704efc 100644 --- a/project_binding.go +++ b/project_binding.go @@ -70,7 +70,7 @@ func (c *MeshStackProviderClient) readProjectBinding(name string, contentType st return nil, nil } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -120,7 +120,7 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi return nil, err } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/status_code_checker.go b/status_code_checker.go new file mode 100644 index 0000000..bd6f24b --- /dev/null +++ b/status_code_checker.go @@ -0,0 +1,13 @@ +package client + +import ( + "net/http" +) + +func isSuccessHTTPStatus(resp *http.Response) bool { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return false + } + + return true +} diff --git a/tag_definition.go b/tag_definition.go index 1855b26..67ff98d 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -107,7 +107,7 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er return nil, fmt.Errorf("failed to read response body: %w", err) } - if res.StatusCode != http.StatusOK { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -156,7 +156,7 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if !isSuccessHTTPStatus(resp) { return nil, fmt.Errorf("failed to read tag definition: %s", resp.Status) } @@ -191,7 +191,7 @@ func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefi } defer resp.Body.Close() - if resp.StatusCode != http.StatusCreated { + if !isSuccessHTTPStatus(resp) { return nil, fmt.Errorf("failed to create tag definition: %s", resp.Status) } @@ -224,7 +224,7 @@ func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefi } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if !isSuccessHTTPStatus(resp) { return nil, fmt.Errorf("failed to update tag definition: %s", resp.Status) } diff --git a/tenant.go b/tenant.go index b83c5e6..d10556d 100644 --- a/tenant.go +++ b/tenant.go @@ -83,7 +83,7 @@ func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, p return nil, nil } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } @@ -121,7 +121,7 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT return nil, err } - if res.StatusCode != 201 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From ba64475975ac78f8111255144f22b0ecd1971386 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Tue, 25 Feb 2025 14:00:41 +0100 Subject: [PATCH 014/200] feature: preview building blocks v2 resources --- buildingblock.go | 6 +- buildingblock_v2.go | 141 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 buildingblock_v2.go diff --git a/buildingblock.go b/buildingblock.go index 41c4252..7e456cd 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -46,9 +46,9 @@ type MeshBuildingBlockSpec struct { } type MeshBuildingBlockIO struct { - Key string `json:"key" tfsdk:"key"` - Value interface{} `json:"value" tfsdk:"value"` - ValueType string `json:"valueType" tfsdk:"value_type"` + Key string `json:"key" tfsdk:"key"` + Value any `json:"value" tfsdk:"value"` + ValueType string `json:"valueType" tfsdk:"value_type"` } type MeshBuildingBlockParent struct { diff --git a/buildingblock_v2.go b/buildingblock_v2.go new file mode 100644 index 0000000..371a672 --- /dev/null +++ b/buildingblock_v2.go @@ -0,0 +1,141 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +const ( + CONTENT_TYPE_BUILDING_BLOCK_V2 = "application/vnd.meshcloud.api.meshbuildingblock.v2-preview.hal+json" +) + +type MeshBuildingBlockV2 struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshBuildingBlockV2Metadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` + Status MeshBuildingBlockV2Status `json:"status" tfsdk:"status"` +} + +type MeshBuildingBlockV2Metadata struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` + MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` +} + +type MeshBuildingBlockV2Spec struct { + BuildingBlockDefinitionVersionRef MeshBuildingBlockV2DefinitionVersionRef `json:"buildingBlockDefinitionVersionRef" tfsdk:"building_block_definition_version_ref"` + TargetRef MeshBuildingBlockV2TargetRef `json:"targetRef" tfsdk:"target_ref"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + + Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` + ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` +} + +type MeshBuildingBlockV2DefinitionVersionRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` +} + +type MeshBuildingBlockV2TargetRef struct { + Kind string `json:"kind" tfsdk:"kind"` + Uuid *string `json:"uuid" tfsdk:"uuid"` + Identifier *string `json:"identifier" tfsdk:"identifier"` +} + +type MeshBuildingBlockV2Create struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` +} + +type MeshBuildingBlockV2Status struct { + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` +} + +func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildingBlockV2, error) { + targetUrl := c.urlForBuildingBlock(uuid) + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var bb MeshBuildingBlockV2 + err = json.Unmarshal(data, &bb) + if err != nil { + return nil, err + } + + return &bb, nil +} + +func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { + payload, err := json.Marshal(bb) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.BuildingBlocks.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK_V2) + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBb MeshBuildingBlockV2 + err = json.Unmarshal(data, &createdBb) + if err != nil { + return nil, err + } + + return &createdBb, nil +} + +func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { + targetUrl := c.urlForBuildingBlock(uuid) + return c.deleteMeshObject(*targetUrl, 202) +} From e6df98a1cf00e8443223df00eb314b24d1d27416 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 17 Apr 2025 13:37:51 +0200 Subject: [PATCH 015/200] feature: source provider configuration from environment --- client.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index bf0ee23..c6c629e 100644 --- a/client.go +++ b/client.go @@ -86,7 +86,9 @@ func (c *MeshStackProviderClient) login() error { res, err := c.httpClient.Do(req) - if err != nil || res.StatusCode != 200 { + if err != nil { + return err + } else if res.StatusCode != 200 { return errors.New(ERROR_AUTHENTICATION_FAILURE) } @@ -174,8 +176,9 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request) (*ht log.Println(req) // add authentication - if c.ensureValidToken() != nil { - return nil, errors.New(ERROR_AUTHENTICATION_FAILURE) + err := c.ensureValidToken() + if err != nil { + return nil, err } req.Header.Set("Authorization", c.token) From b3b3f9a63ea4544bccfc180700861e54abf1797b Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Thu, 31 Jul 2025 15:14:36 +0200 Subject: [PATCH 016/200] fix: allow code inputs in buildingblock resource --- buildingblock.go | 1 + 1 file changed, 1 insertion(+) diff --git a/buildingblock.go b/buildingblock.go index 7e456cd..2867be4 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -16,6 +16,7 @@ const ( MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT = "SINGLE_SELECT" MESH_BUILDING_BLOCK_IO_TYPE_FILE = "FILE" MESH_BUILDING_BLOCK_IO_TYPE_LIST = "LIST" + MESH_BUILDING_BLOCK_IO_TYPE_CODE = "CODE" CONTENT_TYPE_BUILDING_BLOCK = "application/vnd.meshcloud.api.meshbuildingblock.v1.hal+json" ) From 2dc60308c3bf1ff52b2365d0d92f57e0901bc644 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 23 Jul 2025 10:09:40 +0200 Subject: [PATCH 017/200] feat: workspace data source --- client.go | 2 ++ project.go | 2 +- workspace.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 workspace.go diff --git a/client.go b/client.go index c6c629e..7a5e42c 100644 --- a/client.go +++ b/client.go @@ -37,6 +37,7 @@ type endpoints struct { Projects *url.URL `json:"meshprojects"` ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` + Workspaces *url.URL `json:"meshworkspaces"` Tenants *url.URL `json:"meshtenants"` TagDefinitions *url.URL `json:"meshtagdefinitions"` } @@ -63,6 +64,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), + Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), } diff --git a/project.go b/project.go index 683b2ee..eca293a 100644 --- a/project.go +++ b/project.go @@ -67,7 +67,7 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M return nil, err } - if res.StatusCode == 404 { + if res.StatusCode == http.StatusNotFound { return nil, nil } diff --git a/workspace.go b/workspace.go new file mode 100644 index 0000000..7effc64 --- /dev/null +++ b/workspace.go @@ -0,0 +1,69 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_WORKSPACE = "application/vnd.meshcloud.api.meshworkspace.v1.hal+json" + +type MeshWorkspace struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshWorkspaceMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` +} + +type MeshWorkspaceMetadata struct { + Name string `json:"name" tfsdk:"name"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` +} + +type MeshWorkspaceSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +func (c *MeshStackProviderClient) urlForWorkspace(name string) *url.URL { + return c.endpoints.Workspaces.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, error) { + targetUrl := c.urlForWorkspace(name) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil, nil // Not found is not an error + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var workspace MeshWorkspace + err = json.Unmarshal(data, &workspace) + if err != nil { + return nil, err + } + return &workspace, nil +} From 6118432d676c034f8a70731b0e2724dcda86be24 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 23 Jul 2025 13:47:59 +0200 Subject: [PATCH 018/200] feat: workspace resource --- workspace.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/workspace.go b/workspace.go index 7effc64..0ade57c 100644 --- a/workspace.go +++ b/workspace.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "encoding/json" "fmt" "io" @@ -8,7 +9,7 @@ import ( "net/url" ) -const CONTENT_TYPE_WORKSPACE = "application/vnd.meshcloud.api.meshworkspace.v1.hal+json" +const CONTENT_TYPE_WORKSPACE = "application/vnd.meshcloud.api.meshworkspace.v2.hal+json" type MeshWorkspace struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` @@ -18,14 +19,25 @@ type MeshWorkspace struct { } type MeshWorkspaceMetadata struct { - Name string `json:"name" tfsdk:"name"` - CreatedOn string `json:"createdOn" tfsdk:"created_on"` - DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` + Name string `json:"name" tfsdk:"name"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` } type MeshWorkspaceSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Tags map[string][]string `json:"tags" tfsdk:"tags"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + PlatformBuilderAccessEnabled *bool `json:"platformBuilderAccessEnabled,omitempty" tfsdk:"platform_builder_access_enabled"` +} + +type MeshWorkspaceCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshWorkspaceCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` +} +type MeshWorkspaceCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` } func (c *MeshStackProviderClient) urlForWorkspace(name string) *url.URL { @@ -67,3 +79,82 @@ func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, er } return &workspace, nil } + +func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + paylod, err := json.Marshal(workspace) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Workspaces.String(), bytes.NewBuffer(paylod)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) + req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdWorkspace MeshWorkspace + err = json.Unmarshal(data, &createdWorkspace) + if err != nil { + return nil, err + } + return &createdWorkspace, nil +} + +func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + targetUrl := c.urlForWorkspace(name) + + paylod, err := json.Marshal(workspace) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(paylod)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) + req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var updatedWorkspace MeshWorkspace + err = json.Unmarshal(data, &updatedWorkspace) + if err != nil { + return nil, err + } + return &updatedWorkspace, nil +} + +func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { + targetUrl := c.urlForWorkspace(name) + return c.deleteMeshObject(*targetUrl, 204) +} From ef46955a5b3e98aeebebed27cc45259983a365ab Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Thu, 7 Nov 2024 09:48:48 +0100 Subject: [PATCH 019/200] feat: meshstack_tenant_v4 resource --- tenant_v4.go | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 tenant_v4.go diff --git a/tenant_v4.go b/tenant_v4.go new file mode 100644 index 0000000..b4974c4 --- /dev/null +++ b/tenant_v4.go @@ -0,0 +1,143 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_TENANT_V4 = "application/vnd.meshcloud.api.meshtenant.v4.hal+json" + +type MeshTenantV4 struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshTenantMetadataV4 `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantSpecV4 `json:"spec" tfsdk:"spec"` + Status MeshTenantStatusV4 `json:"status" tfsdk:"status"` +} + +type MeshTenantMetadataV4 struct { + UUID string `json:"uuid" tfsdk:"uuid"` + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` + CreatedOn *string `json:"createdOn" tfsdk:"created_on"` +} + +type MeshTenantSpecV4 struct { + PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` + LocalId *string `json:"localId" tfsdk:"local_id"` + LandingZoneIdentifier string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` + Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` +} + +type MeshTenantStatusV4 struct { + Tags map[string][]string `json:"tags" tfsdk:"tags"` + LastReplicated *string `json:"lastReplicated" tfsdk:"last_replicated"` + CurrentReplicationStatus string `json:"currentReplicationStatus" tfsdk:"current_replication_status"` +} + +type MeshTenantCreateV4 struct { + Metadata MeshTenantCreateMetadataV4 `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantCreateSpecV4 `json:"spec" tfsdk:"spec"` +} + +type MeshTenantCreateMetadataV4 struct { + UUID string `json:"uuid" tfsdk:"uuid"` + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshTenantCreateSpecV4 struct { + PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` + LocalId *string `json:"localId" tfsdk:"local_id"` + LandingZoneIdentifier string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` + Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` +} + +func (c *MeshStackProviderClient) urlForTenantV4(uuid string) *url.URL { + return c.endpoints.Tenants.JoinPath(uuid) +} + +func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, error) { + targetUrl := c.urlForTenantV4(uuid) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var tenant MeshTenantV4 + err = json.Unmarshal(data, &tenant) + if err != nil { + return nil, err + } + + return &tenant, nil +} + +func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantCreateV4) (*MeshTenantV4, error) { + payload, err := json.Marshal(tenant) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Tenants.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_TENANT_V4) + req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) + + 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, err + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdTenant MeshTenantV4 + err = json.Unmarshal(data, &createdTenant) + if err != nil { + return nil, err + } + + return &createdTenant, nil +} + +func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { + targetUrl := c.urlForTenantV4(uuid) + return c.deleteMeshObject(*targetUrl, 202) +} From d71487c56e0bfc0e0250d473dee42e08478ff6f7 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Mon, 28 Jul 2025 16:10:14 +0200 Subject: [PATCH 020/200] fix: adapt tenant v4 client to actual implementation --- tenant_v4.go | 63 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/tenant_v4.go b/tenant_v4.go index b4974c4..4d99840 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -9,53 +9,54 @@ import ( "net/url" ) -const CONTENT_TYPE_TENANT_V4 = "application/vnd.meshcloud.api.meshtenant.v4.hal+json" +const CONTENT_TYPE_TENANT_V4 = "application/vnd.meshcloud.api.meshtenant.v4-preview.hal+json" type MeshTenantV4 struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshTenantMetadataV4 `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantSpecV4 `json:"spec" tfsdk:"spec"` - Status MeshTenantStatusV4 `json:"status" tfsdk:"status"` + Metadata MeshTenantV4Metadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantV4Spec `json:"spec" tfsdk:"spec"` + Status MeshTenantV4Status `json:"status" tfsdk:"status"` } -type MeshTenantMetadataV4 struct { - UUID string `json:"uuid" tfsdk:"uuid"` - OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` - CreatedOn *string `json:"createdOn" tfsdk:"created_on"` +type MeshTenantV4Metadata struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` } -type MeshTenantSpecV4 struct { - PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` - LocalId *string `json:"localId" tfsdk:"local_id"` - LandingZoneIdentifier string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` - Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` +type MeshTenantV4Spec struct { + PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` + Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } -type MeshTenantStatusV4 struct { - Tags map[string][]string `json:"tags" tfsdk:"tags"` - LastReplicated *string `json:"lastReplicated" tfsdk:"last_replicated"` - CurrentReplicationStatus string `json:"currentReplicationStatus" tfsdk:"current_replication_status"` +type MeshTenantV4Status struct { + TenantName string `json:"tenantName" tfsdk:"tenant_name"` + PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` + PlatformWorkspaceIdentifier *string `json:"platformWorkspaceIdentifier" tfsdk:"platform_workspace_identifier"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` } -type MeshTenantCreateV4 struct { - Metadata MeshTenantCreateMetadataV4 `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantCreateSpecV4 `json:"spec" tfsdk:"spec"` +type MeshTenantV4Create struct { + Metadata MeshTenantV4CreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantV4CreateSpec `json:"spec" tfsdk:"spec"` } -type MeshTenantCreateMetadataV4 struct { - UUID string `json:"uuid" tfsdk:"uuid"` +type MeshTenantV4CreateMetadata struct { OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -type MeshTenantCreateSpecV4 struct { - PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` - LocalId *string `json:"localId" tfsdk:"local_id"` - LandingZoneIdentifier string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` - Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` +type MeshTenantV4CreateSpec struct { + PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` + LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } func (c *MeshStackProviderClient) urlForTenantV4(uuid string) *url.URL { @@ -99,7 +100,7 @@ func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, erro return &tenant, nil } -func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantCreateV4) (*MeshTenantV4, error) { +func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { payload, err := json.Marshal(tenant) if err != nil { return nil, err @@ -124,7 +125,7 @@ func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantCreateV4) (*M return nil, err } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From d9197939167db79479b6f9c757ed08a567899401 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Mon, 28 Jul 2025 16:22:46 +0200 Subject: [PATCH 021/200] fix: adapt tenant_v4 resource to actual implementation --- tenant_v4.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tenant_v4.go b/tenant_v4.go index 4d99840..00991b5 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -87,7 +87,7 @@ func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, erro return nil, nil } - if res.StatusCode != 200 { + if !isSuccessHTTPStatus(res) { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From 9ac4d63e8e57c55159fe4805c366184b57d1c59c Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Mon, 11 Aug 2025 19:04:20 +0200 Subject: [PATCH 022/200] feat: buildingblock v2 polling for completion --- buildingblock_v2.go | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 371a672..fbb81e4 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -2,10 +2,14 @@ package client import ( "bytes" + "context" "encoding/json" "fmt" "io" "net/http" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) const ( @@ -139,3 +143,72 @@ func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { targetUrl := c.urlForBuildingBlock(uuid) return c.deleteMeshObject(*targetUrl, 202) } + +// PollBuildingBlockV2UntilCompletion polls a building block v2 until it reaches a terminal state (SUCCEEDED or FAILED) +// Returns the final building block state or an error if polling fails or times out +func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { + var result *MeshBuildingBlockV2 + + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2CompletionFunc(ctx, uuid, &result)) + if err != nil { + return nil, err + } + + return result, nil +} + +// waitForBuildingBlockV2CompletionFunc returns a RetryFunc that checks building block completion status +func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(ctx context.Context, uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { + return func() *retry.RetryError { + current, err := c.ReadBuildingBlockV2(uuid) + if err != nil { + return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for completion: %w", err)) + } + + if current == nil { + return retry.NonRetryableError(fmt.Errorf("building block was not found while waiting for completion")) + } + + // Check if we've reached a terminal state + status := current.Status.Status + switch status { + case "SUCCEEDED": + *result = current + return nil // Success, stop retrying + case "FAILED": + return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state", uuid)) + } + + // Not done yet, continue polling + return retry.RetryableError(fmt.Errorf("waiting for building block %s to complete: currently in %s state", uuid, status)) + } +} + +// PollBuildingBlockV2UntilDeletion polls a building block v2 until it is deleted (not found) +// Returns nil on successful deletion or an error if polling fails or times out +func (c *MeshStackProviderClient) PollBuildingBlockV2UntilDeletion(ctx context.Context, uuid string) error { + return retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2DeletionFunc(uuid)) +} + +// waitForBuildingBlockV2DeletionFunc returns a RetryFunc that checks building block deletion status +func (c *MeshStackProviderClient) waitForBuildingBlockV2DeletionFunc(uuid string) retry.RetryFunc { + return func() *retry.RetryError { + current, err := c.ReadBuildingBlockV2(uuid) + if err != nil { + return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for deletion: %w", err)) + } + + // If building block is not found, deletion is complete + if current == nil { + return nil // Success, stop retrying + } + + // If building block is in FAILED state during deletion, consider it a terminal state + if current.Status.Status == "FAILED" { + return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state during deletion", uuid)) + } + + // Not done yet, continue polling + return retry.RetryableError(fmt.Errorf("waiting for building block %s to be deleted: currently in %s state", uuid, current.Status.Status)) + } +} From 0802352270990f51dac0cd72b11f648bf1469da5 Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Wed, 13 Aug 2025 13:02:26 +0200 Subject: [PATCH 023/200] feat: tenant v4 polling for completion --- buildingblock_v2.go | 8 +++--- tenant_v4.go | 64 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index fbb81e4..5bc6ffb 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -144,12 +144,12 @@ func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { return c.deleteMeshObject(*targetUrl, 202) } -// PollBuildingBlockV2UntilCompletion polls a building block v2 until it reaches a terminal state (SUCCEEDED or FAILED) +// PollBuildingBlockV2UntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) // Returns the final building block state or an error if polling fails or times out func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { var result *MeshBuildingBlockV2 - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2CompletionFunc(ctx, uuid, &result)) + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2CompletionFunc(uuid, &result)) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context } // waitForBuildingBlockV2CompletionFunc returns a RetryFunc that checks building block completion status -func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(ctx context.Context, uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { +func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { return func() *retry.RetryError { current, err := c.ReadBuildingBlockV2(uuid) if err != nil { @@ -184,7 +184,7 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(ctx conte } } -// PollBuildingBlockV2UntilDeletion polls a building block v2 until it is deleted (not found) +// PollBuildingBlockV2UntilDeletion polls a building block until it is deleted (not found) // Returns nil on successful deletion or an error if polling fails or times out func (c *MeshStackProviderClient) PollBuildingBlockV2UntilDeletion(ctx context.Context, uuid string) error { return retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2DeletionFunc(uuid)) diff --git a/tenant_v4.go b/tenant_v4.go index 00991b5..4768c69 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -2,11 +2,15 @@ package client import ( "bytes" + "context" "encoding/json" "fmt" "io" "net/http" "net/url" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) const CONTENT_TYPE_TENANT_V4 = "application/vnd.meshcloud.api.meshtenant.v4-preview.hal+json" @@ -142,3 +146,63 @@ func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { targetUrl := c.urlForTenantV4(uuid) return c.deleteMeshObject(*targetUrl, 202) } + +// PollTenantV4UntilCreation polls a tenant until creation completes (platformTenantId is set) +// Returns the final tenant state or an error if polling fails or times out +func (c *MeshStackProviderClient) PollTenantV4UntilCreation(ctx context.Context, uuid string) (*MeshTenantV4, error) { + var result *MeshTenantV4 + + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForTenantV4CreationFunc(uuid, &result)) + if err != nil { + return nil, err + } + + return result, nil +} + +// waitForTenantV4CreationFunc returns a RetryFunc that checks tenant creation status +func (c *MeshStackProviderClient) waitForTenantV4CreationFunc(uuid string, result **MeshTenantV4) retry.RetryFunc { + return func() *retry.RetryError { + current, err := c.ReadTenantV4(uuid) + if err != nil { + return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for creation: %w", err)) + } + + if current == nil { + return retry.NonRetryableError(fmt.Errorf("tenant was not found while waiting for creation")) + } + + // Check if creation is complete (platformTenantId is set) + if current.Spec.PlatformTenantId != nil && *current.Spec.PlatformTenantId != "" { + *result = current + return nil // Success, stop retrying + } + + // Not done yet, continue polling + return retry.RetryableError(fmt.Errorf("waiting for tenant %s creation to complete: platformTenantId not yet set", uuid)) + } +} + +// PollTenantV4UntilDeletion polls a tenant until it is deleted (not found) +// Returns nil on successful deletion or an error if polling fails or times out +func (c *MeshStackProviderClient) PollTenantV4UntilDeletion(ctx context.Context, uuid string) error { + return retry.RetryContext(ctx, 30*time.Minute, c.waitForTenantV4DeletionFunc(uuid)) +} + +// waitForTenantV4DeletionFunc returns a RetryFunc that checks tenant deletion status +func (c *MeshStackProviderClient) waitForTenantV4DeletionFunc(uuid string) retry.RetryFunc { + return func() *retry.RetryError { + current, err := c.ReadTenantV4(uuid) + if err != nil { + return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for deletion: %w", err)) + } + + // If tenant is not found, deletion is complete + if current == nil { + return nil // Success, stop retrying + } + + // Not done yet, continue polling + return retry.RetryableError(fmt.Errorf("waiting for tenant %s to be deleted: still present", uuid)) + } +} From c67b1c9b08a471638539fd9e5f94368e3435a9c5 Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Thu, 14 Aug 2025 10:54:59 +0200 Subject: [PATCH 024/200] chore: example for building_block_v2 includes adaptations from PR remarks --- buildingblock_v2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 5bc6ffb..be23299 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -205,7 +205,7 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2DeletionFunc(uuid string // If building block is in FAILED state during deletion, consider it a terminal state if current.Status.Status == "FAILED" { - return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state during deletion", uuid)) + return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", uuid)) } // Not done yet, continue polling From 3d2ffc92ae35bb7a587c5087851a3bae6670cdde Mon Sep 17 00:00:00 2001 From: OliverEsoterik Date: Mon, 18 Aug 2025 18:25:11 +0200 Subject: [PATCH 025/200] add initial workspace bindings --- client.go | 3 + workspace_binding.go | 133 +++++++++++++++++++++++++++++++++++++ workspace_group_binding.go | 26 ++++++++ workspace_user_binding.go | 26 ++++++++ 4 files changed, 188 insertions(+) create mode 100644 workspace_binding.go create mode 100644 workspace_group_binding.go create mode 100644 workspace_user_binding.go diff --git a/client.go b/client.go index 7a5e42c..d6ad7b9 100644 --- a/client.go +++ b/client.go @@ -38,6 +38,7 @@ type endpoints struct { ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` Workspaces *url.URL `json:"meshworkspaces"` + WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` Tenants *url.URL `json:"meshtenants"` TagDefinitions *url.URL `json:"meshtagdefinitions"` } @@ -65,6 +66,8 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), + WorkspaceUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "userbindings"), + WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), } diff --git a/workspace_binding.go b/workspace_binding.go new file mode 100644 index 0000000..d4dc955 --- /dev/null +++ b/workspace_binding.go @@ -0,0 +1,133 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type MeshWorkspaceBinding struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshProjectBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshSubject `json:"subject" tfsdk:"subject"` +} + +type MeshWorkspaceBindingMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshWorkspaceRoleRef struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshWorkspaceTargetRef struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshSubject struct { + Name string `json:"name" tfsdk:"name"` +} + +func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType string) (*MeshWorkspaceBinding, error) { + var targetUrl *url.URL + switch contentType { + case CONTENT_TYPE_WORKSPACE_USER_BINDING: + targetUrl = c.urlForWorkspaceUserBinding(name) + + case CONTENT_TYPE_WORKSPACE_GROUP_BINDING: + targetUrl = c.urlForWorkspaceGroupBinding(name) + + default: + return nil, fmt.Errorf("Unexpected content type: %s", contentType) + } + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", contentType) + + 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, err + } + + if res.StatusCode == 404 { + return nil, nil + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var binding MeshWorkspaceBinding + err = json.Unmarshal(data, &binding) + if err != nil { + return nil, err + } + + return &binding, nil +} + +func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceBinding, contentType string) (*MeshWorkspaceBinding, error) { + var targetUrl *url.URL + switch contentType { + case CONTENT_TYPE_WORKSPACE_USER_BINDING: + targetUrl = c.endpoints.WorkspaceUserBindings + + case CONTENT_TYPE_WORKSPACE_GROUP_BINDING: + targetUrl = c.endpoints.WorkspaceGroupBindings + + default: + return nil, fmt.Errorf("Unexpected content type: %s", contentType) + } + + payload, err := json.Marshal(binding) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept", contentType) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBinding MeshProjectBinding + err = json.Unmarshal(data, &createdBinding) + if err != nil { + return nil, err + } + + return &createdBinding, nil +} diff --git a/workspace_group_binding.go b/workspace_group_binding.go new file mode 100644 index 0000000..24887c8 --- /dev/null +++ b/workspace_group_binding.go @@ -0,0 +1,26 @@ +package client + +import ( + "net/url" +) + +const CONTENT_TYPE_WORKSPACE_GROUP_BINDING = "application/vnd.meshcloud.api.meshworkspacegroupbinding.v2.hal+json" + +type MeshWorkspaceGroupBinding = MeshWorkspaceBinding + +func (c *MeshStackProviderClient) urlForWorkspaceGroupBinding(name string) *url.URL { + return c.endpoints.WorkspaceGroupBindings.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadPWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { + return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { + return c.createWorkspaceBinding(binding, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { + targetUrl := c.urlForWorkspaceGroupBinding(name) + return c.deleteMeshObject(*targetUrl, 204) +} diff --git a/workspace_user_binding.go b/workspace_user_binding.go new file mode 100644 index 0000000..e50db18 --- /dev/null +++ b/workspace_user_binding.go @@ -0,0 +1,26 @@ +package client + +import ( + "net/url" +) + +const CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshworkspaceuserbinding.v2.hal+json" + +type MeshProjectUserBinding = MeshProjectBinding + +func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL { + return c.endpoints.ProjectUserBindings.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { + return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_USER_BINDING) +} + +func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { + return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_USER_BINDING) +} + +func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { + targetUrl := c.urlForPojectUserBinding(name) + return c.deleteMeshObject(*targetUrl, 204) +} From 36d7b9115141bcd9b0822009d22a28ca012bf28e Mon Sep 17 00:00:00 2001 From: OliverEsoterik Date: Mon, 18 Aug 2025 18:36:48 +0200 Subject: [PATCH 026/200] additional changes from project to workspace, remove duplicates --- workspace_binding.go | 10 +++++----- workspace_user_binding.go | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/workspace_binding.go b/workspace_binding.go index d4dc955..f9073f8 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -12,10 +12,10 @@ import ( type MeshWorkspaceBinding struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshProjectBindingMetadata `json:"metadata" tfsdk:"metadata"` - RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` - TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshSubject `json:"subject" tfsdk:"subject"` + Metadata MeshWorkspaceBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshWorkspaceRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshWorkspaceTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` } type MeshWorkspaceBindingMetadata struct { @@ -30,7 +30,7 @@ type MeshWorkspaceTargetRef struct { Name string `json:"name" tfsdk:"name"` } -type MeshSubject struct { +type MeshWorkspaceSubject struct { Name string `json:"name" tfsdk:"name"` } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index e50db18..dbd7d77 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -4,23 +4,23 @@ import ( "net/url" ) -const CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshworkspaceuserbinding.v2.hal+json" +const CONTENT_TYPE_WORKSPACE_USER_BINDING = "application/vnd.meshcloud.api.meshworkspaceuserbinding.v2.hal+json" -type MeshProjectUserBinding = MeshProjectBinding +type MeshWorkspaceUserBinding = MeshProjectBinding -func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL { - return c.endpoints.ProjectUserBindings.JoinPath(name) +func (c *MeshStackProviderClient) urlForWorkspaceUserBinding(name string) *url.URL { + return c.endpoints.WorkspaceUserBindings.JoinPath(name) } -func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { - return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_USER_BINDING) +func (c *MeshStackProviderClient) ReadWorkspaceUserBinding(name string) (*MeshWorkspaceUserBinding, error) { + return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_USER_BINDING) } -func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_USER_BINDING) +func (c *MeshStackProviderClient) CreateWorkspaceUserBinding(binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { + return c.createWorkspaceBinding(binding, CONTENT_TYPE_WORKSPACE_USER_BINDING) } -func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { - targetUrl := c.urlForPojectUserBinding(name) +func (c *MeshStackProviderClient) DeleteWorkspaceUserBinding(name string) error { + targetUrl := c.urlForWorkspaceUserBinding(name) return c.deleteMeshObject(*targetUrl, 204) } From 0c6813b29ea954c9d79b0dcfd61b2a95e12079df Mon Sep 17 00:00:00 2001 From: OliverEsoterik Date: Mon, 18 Aug 2025 18:44:09 +0200 Subject: [PATCH 027/200] duplicates and rrs resolved --- client.go | 1 + workspace_binding.go | 2 +- workspace_user_binding.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index d6ad7b9..be64f4c 100644 --- a/client.go +++ b/client.go @@ -38,6 +38,7 @@ type endpoints struct { ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` Workspaces *url.URL `json:"meshworkspaces"` + WorkspaceUserBindings *url.URL `json:"meshworkspaceuserbindings"` WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` Tenants *url.URL `json:"meshtenants"` TagDefinitions *url.URL `json:"meshtagdefinitions"` diff --git a/workspace_binding.go b/workspace_binding.go index f9073f8..eb4718f 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -123,7 +123,7 @@ func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceB return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } - var createdBinding MeshProjectBinding + var createdBinding MeshWorkspaceBinding err = json.Unmarshal(data, &createdBinding) if err != nil { return nil, err diff --git a/workspace_user_binding.go b/workspace_user_binding.go index dbd7d77..3019df8 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -6,7 +6,7 @@ import ( const CONTENT_TYPE_WORKSPACE_USER_BINDING = "application/vnd.meshcloud.api.meshworkspaceuserbinding.v2.hal+json" -type MeshWorkspaceUserBinding = MeshProjectBinding +type MeshWorkspaceUserBinding = MeshWorkspaceBinding func (c *MeshStackProviderClient) urlForWorkspaceUserBinding(name string) *url.URL { return c.endpoints.WorkspaceUserBindings.JoinPath(name) From 5eb5713cd03fa495ff26a19767149e4e513771fb Mon Sep 17 00:00:00 2001 From: OliverEsoterik Date: Mon, 18 Aug 2025 18:53:00 +0200 Subject: [PATCH 028/200] remove typo in workspace group bindings --- workspace_group_binding.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 24887c8..0f3e7d6 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -12,7 +12,7 @@ func (c *MeshStackProviderClient) urlForWorkspaceGroupBinding(name string) *url. return c.endpoints.WorkspaceGroupBindings.JoinPath(name) } -func (c *MeshStackProviderClient) ReadPWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { +func (c *MeshStackProviderClient) ReadWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) } From 7f0a4f8230fa50a96a0f318f1242504062dfe561 Mon Sep 17 00:00:00 2001 From: OliverEsoterik Date: Mon, 18 Aug 2025 18:56:55 +0200 Subject: [PATCH 029/200] fmt --- client.go | 36 ++++++++++++++++++------------------ workspace_binding.go | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/client.go b/client.go index be64f4c..1529c4c 100644 --- a/client.go +++ b/client.go @@ -33,15 +33,15 @@ type MeshStackProviderClient struct { } type endpoints struct { - BuildingBlocks *url.URL `json:"meshbuildingblocks"` - Projects *url.URL `json:"meshprojects"` - ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` - ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` - Workspaces *url.URL `json:"meshworkspaces"` - WorkspaceUserBindings *url.URL `json:"meshworkspaceuserbindings"` - WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` - Tenants *url.URL `json:"meshtenants"` - TagDefinitions *url.URL `json:"meshtagdefinitions"` + BuildingBlocks *url.URL `json:"meshbuildingblocks"` + Projects *url.URL `json:"meshprojects"` + ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` + ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` + Workspaces *url.URL `json:"meshworkspaces"` + WorkspaceUserBindings *url.URL `json:"meshworkspaceuserbindings"` + WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` + Tenants *url.URL `json:"meshtenants"` + TagDefinitions *url.URL `json:"meshtagdefinitions"` } type loginResponse struct { @@ -62,15 +62,15 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro // TODO: lookup endpoints client.endpoints = endpoints{ - BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), - Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), - ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), - ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), - Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), - WorkspaceUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "userbindings"), - WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), - Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), - TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), + BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), + Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), + ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), + ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), + Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), + WorkspaceUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "userbindings"), + WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), + Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), + TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), } return client, nil diff --git a/workspace_binding.go b/workspace_binding.go index eb4718f..ffef895 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -10,12 +10,12 @@ import ( ) type MeshWorkspaceBinding struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` Metadata MeshWorkspaceBindingMetadata `json:"metadata" tfsdk:"metadata"` RoleRef MeshWorkspaceRoleRef `json:"roleRef" tfsdk:"role_ref"` TargetRef MeshWorkspaceTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` + Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` } type MeshWorkspaceBindingMetadata struct { From e36aba571a530437a6cfc5f978b6244cf416065d Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Mon, 18 Aug 2025 22:41:56 +0200 Subject: [PATCH 030/200] refactor: buildingblock status constants --- buildingblock_v2.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index be23299..bf2bc55 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -14,6 +14,14 @@ import ( const ( CONTENT_TYPE_BUILDING_BLOCK_V2 = "application/vnd.meshcloud.api.meshbuildingblock.v2-preview.hal+json" + + // Building Block Status Constants + BUILDING_BLOCK_STATUS_WAITING_FOR_DEPENDENT_INPUT = "WAITING_FOR_DEPENDENT_INPUT" + BUILDING_BLOCK_STATUS_WAITING_FOR_OPERATOR_INPUT = "WAITING_FOR_OPERATOR_INPUT" + BUILDING_BLOCK_STATUS_PENDING = "PENDING" + BUILDING_BLOCK_STATUS_IN_PROGRESS = "IN_PROGRESS" + BUILDING_BLOCK_STATUS_SUCCEEDED = "SUCCEEDED" + BUILDING_BLOCK_STATUS_FAILED = "FAILED" ) type MeshBuildingBlockV2 struct { @@ -172,10 +180,10 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid stri // Check if we've reached a terminal state status := current.Status.Status switch status { - case "SUCCEEDED": + case BUILDING_BLOCK_STATUS_SUCCEEDED: *result = current return nil // Success, stop retrying - case "FAILED": + case BUILDING_BLOCK_STATUS_FAILED: return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state", uuid)) } @@ -204,7 +212,7 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2DeletionFunc(uuid string } // If building block is in FAILED state during deletion, consider it a terminal state - if current.Status.Status == "FAILED" { + if current.Status.Status == BUILDING_BLOCK_STATUS_FAILED { return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", uuid)) } From d95061152c6ac8c78b12dea12025beeca2285b2f Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 29 Aug 2025 13:44:38 +0200 Subject: [PATCH 031/200] refactor: move type out of loop --- tag_definition.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tag_definition.go b/tag_definition.go index 67ff98d..b8d40ee 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -83,6 +83,18 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er targetUrl := c.endpoints.TagDefinitions query := targetUrl.Query() + type tagsResponse 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"` + } + for { query.Set("page", fmt.Sprintf("%d", pageNumber)) @@ -111,18 +123,7 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er 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"` - } - + var response tagsResponse err = json.Unmarshal(data, &response) if err != nil { return nil, err From a46ce61ff61898765d2c943ea7831f3a2f4c3b13 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 29 Aug 2025 15:25:30 +0200 Subject: [PATCH 032/200] fix: use pointers for optional tag value fields This ensures we can differentiate between not setting a default and setting a default of the empty value, e.g. 0. gh-37 --- tag_definition.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tag_definition.go b/tag_definition.go index b8d40ee..dd36b22 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -45,31 +45,31 @@ type MeshTagDefinitionValueType struct { } type TagValueString struct { - DefaultValue string `json:"defaultValue,omitempty" tfsdk:"default_value"` - ValidationRegex string `json:"validationRegex,omitempty" tfsdk:"validation_regex"` + 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"` + 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"` + DefaultValue *int64 `json:"defaultValue,omitempty" tfsdk:"default_value"` } type TagValueNumber struct { - DefaultValue float64 `json:"defaultValue,omitempty" tfsdk:"default_value"` + 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"` + 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"` + Options []string `json:"options,omitempty" tfsdk:"options"` + DefaultValue *[]string `json:"defaultValue,omitempty" tfsdk:"default_value"` } func (c *MeshStackProviderClient) urlForTagDefinition(name string) *url.URL { From 15dcd28862fda1132d2ce58cff49f1009c889461 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 29 Aug 2025 16:22:20 +0200 Subject: [PATCH 033/200] fix: add missing replicationKey to tag definition gh-30 --- tag_definition.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tag_definition.go b/tag_definition.go index dd36b22..7916b9c 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -24,15 +24,16 @@ type MeshTagDefinitionMetadata struct { } 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"` + 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"` + ReplicationKey *string `json:"replicationKey,omitempty" tfsdk:"replication_key"` } type MeshTagDefinitionValueType struct { From ac3b80b280621226d85677e35ac6cec11aec64ca Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 18 Sep 2025 11:28:24 +0200 Subject: [PATCH 034/200] chore: format all code according to go-fmt --- client.go | 36 +++++++++++++------------- workspace_binding.go | 2 +- workspace_group_binding.go | 52 +++++++++++++++++++------------------- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/client.go b/client.go index 1529c4c..7c505fb 100644 --- a/client.go +++ b/client.go @@ -33,15 +33,15 @@ type MeshStackProviderClient struct { } type endpoints struct { - BuildingBlocks *url.URL `json:"meshbuildingblocks"` - Projects *url.URL `json:"meshprojects"` - ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` - ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` - Workspaces *url.URL `json:"meshworkspaces"` - WorkspaceUserBindings *url.URL `json:"meshworkspaceuserbindings"` - WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` - Tenants *url.URL `json:"meshtenants"` - TagDefinitions *url.URL `json:"meshtagdefinitions"` + BuildingBlocks *url.URL `json:"meshbuildingblocks"` + Projects *url.URL `json:"meshprojects"` + ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` + ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` + Workspaces *url.URL `json:"meshworkspaces"` + WorkspaceUserBindings *url.URL `json:"meshworkspaceuserbindings"` + WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` + Tenants *url.URL `json:"meshtenants"` + TagDefinitions *url.URL `json:"meshtagdefinitions"` } type loginResponse struct { @@ -62,15 +62,15 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro // TODO: lookup endpoints client.endpoints = endpoints{ - BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), - Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), - ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), - ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), - Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), - WorkspaceUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "userbindings"), - WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), - Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), - TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), + BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), + Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), + ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), + ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), + Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), + WorkspaceUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "userbindings"), + WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), + Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), + TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), } return client, nil diff --git a/workspace_binding.go b/workspace_binding.go index ffef895..a03b586 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -27,7 +27,7 @@ type MeshWorkspaceRoleRef struct { } type MeshWorkspaceTargetRef struct { - Name string `json:"name" tfsdk:"name"` + Name string `json:"name" tfsdk:"name"` } type MeshWorkspaceSubject struct { diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 0f3e7d6..a32a582 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -1,26 +1,26 @@ -package client - -import ( - "net/url" -) - -const CONTENT_TYPE_WORKSPACE_GROUP_BINDING = "application/vnd.meshcloud.api.meshworkspacegroupbinding.v2.hal+json" - -type MeshWorkspaceGroupBinding = MeshWorkspaceBinding - -func (c *MeshStackProviderClient) urlForWorkspaceGroupBinding(name string) *url.URL { - return c.endpoints.WorkspaceGroupBindings.JoinPath(name) -} - -func (c *MeshStackProviderClient) ReadWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { - return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) -} - -func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { - return c.createWorkspaceBinding(binding, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) -} - -func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { - targetUrl := c.urlForWorkspaceGroupBinding(name) - return c.deleteMeshObject(*targetUrl, 204) -} +package client + +import ( + "net/url" +) + +const CONTENT_TYPE_WORKSPACE_GROUP_BINDING = "application/vnd.meshcloud.api.meshworkspacegroupbinding.v2.hal+json" + +type MeshWorkspaceGroupBinding = MeshWorkspaceBinding + +func (c *MeshStackProviderClient) urlForWorkspaceGroupBinding(name string) *url.URL { + return c.endpoints.WorkspaceGroupBindings.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { + return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { + return c.createWorkspaceBinding(binding, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) +} + +func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { + targetUrl := c.urlForWorkspaceGroupBinding(name) + return c.deleteMeshObject(*targetUrl, 204) +} From f8e67de96633848854f6b55867bddbee3b44b2e4 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 18 Sep 2025 12:39:30 +0200 Subject: [PATCH 035/200] chore: fix typo in workspace.go (paylod) --- workspace.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/workspace.go b/workspace.go index 0ade57c..bdf878f 100644 --- a/workspace.go +++ b/workspace.go @@ -81,12 +81,12 @@ func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, er } func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - paylod, err := json.Marshal(workspace) + payload, err := json.Marshal(workspace) if err != nil { return nil, err } - req, err := http.NewRequest("POST", c.endpoints.Workspaces.String(), bytes.NewBuffer(paylod)) + req, err := http.NewRequest("POST", c.endpoints.Workspaces.String(), bytes.NewBuffer(payload)) if err != nil { return nil, err } @@ -119,12 +119,12 @@ func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { targetUrl := c.urlForWorkspace(name) - paylod, err := json.Marshal(workspace) + payload, err := json.Marshal(workspace) if err != nil { return nil, err } - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(paylod)) + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) if err != nil { return nil, err } From f4482d080eb07d2c92b75213829732788a1c0942 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Wed, 10 Sep 2025 10:54:40 +0200 Subject: [PATCH 036/200] feat: support meshLandingZones CU-86c57xgjy --- client.go | 2 + landingzone.go | 179 ++++++++++++++++++++++++++++++ platform_properties_aks.go | 10 ++ platform_properties_aws.go | 14 +++ platform_properties_azure.go | 17 +++ platform_properties_azurerg.go | 18 +++ platform_properties_gcp.go | 12 ++ platform_properties_kubernetes.go | 5 + platform_properties_openshift.go | 5 + project_binding.go | 7 ++ 10 files changed, 269 insertions(+) create mode 100644 landingzone.go create mode 100644 platform_properties_aks.go create mode 100644 platform_properties_aws.go create mode 100644 platform_properties_azure.go create mode 100644 platform_properties_azurerg.go create mode 100644 platform_properties_gcp.go create mode 100644 platform_properties_kubernetes.go create mode 100644 platform_properties_openshift.go diff --git a/client.go b/client.go index 7c505fb..6d06efa 100644 --- a/client.go +++ b/client.go @@ -42,6 +42,7 @@ type endpoints struct { WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` Tenants *url.URL `json:"meshtenants"` TagDefinitions *url.URL `json:"meshtagdefinitions"` + LandingZones *url.URL `json:"meshlandingzones"` } type loginResponse struct { @@ -71,6 +72,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), + LandingZones: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlandingzones"), } return client, nil diff --git a/landingzone.go b/landingzone.go new file mode 100644 index 0000000..ec4d097 --- /dev/null +++ b/landingzone.go @@ -0,0 +1,179 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_LANDINGZONE = "application/vnd.meshcloud.api.meshlandingzone.v1-preview.hal+json" + +type MeshLandingZone struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` +} + +type MeshLandingZoneMetadata struct { + Name string `json:"name" tfsdk:"name"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +type MeshLandingZoneSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` + AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` + InfoLink string `json:"infoLink" tfsdk:"info_link"` + PlatformRef PlatformRef `json:"platformRef" tfsdk:"platform_ref"` + PlatformProperties *PlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` +} + +type PlatformRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind" tfsdk:"kind"` +} + +type PlatformProperties struct { + Type string `json:"type" tfsdk:"type"` + AWS *AwsPlatformProperties `json:"aws" tfsdk:"aws"` + AKS *AksPlatformProperties `json:"aks" tfsdk:"aks"` + Azure *AzurePlatformProperties `json:"azure" tfsdk:"azure"` + AzureRG *AzureRgPlatformProperties `json:"azurerg" tfsdk:"azurerg"` + GCP *GcpPlatformProperties `json:"gcp" tfsdk:"gcp"` + Kubernetes *KubernetesPlatformProperties `json:"kubernetes" tfsdk:"kubernetes"` + OpenShift *OpenShiftPlatformProperties `json:"openshift" tfsdk:"openshift"` +} + +type MeshLandingZoneCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshLandingZoneCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` +} +type MeshLandingZoneCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +func (c *MeshStackProviderClient) urlForLandingZone(name string) *url.URL { + return c.endpoints.LandingZones.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadLandingZone(name string) (*MeshLandingZone, error) { + targetUrl := c.urlForLandingZone(name) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil, nil // Not found is not an error + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var landingZone MeshLandingZone + err = json.Unmarshal(data, &landingZone) + if err != nil { + return nil, err + } + return &landingZone, nil +} + +func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { + payload, err := json.Marshal(landingZone) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.LandingZones.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) + req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdLandingZone MeshLandingZone + err = json.Unmarshal(data, &createdLandingZone) + if err != nil { + return nil, err + } + return &createdLandingZone, nil +} + +func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { + targetUrl := c.urlForLandingZone(name) + + payload, err := json.Marshal(landingZone) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) + req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var updatedLandingZone MeshLandingZone + err = json.Unmarshal(data, &updatedLandingZone) + if err != nil { + return nil, err + } + return &updatedLandingZone, nil +} + +func (c *MeshStackProviderClient) DeleteLandingZone(name string) error { + targetUrl := c.urlForLandingZone(name) + return c.deleteMeshObject(*targetUrl, 204) +} diff --git a/platform_properties_aks.go b/platform_properties_aks.go new file mode 100644 index 0000000..4599a5f --- /dev/null +++ b/platform_properties_aks.go @@ -0,0 +1,10 @@ +package client + +type AksPlatformProperties struct { + KubernetesRoleMappings []KubernetesRoleMapping `json:"kubernetesRoleMappings" tfsdk:"kubernetes_role_mappings"` +} + +type KubernetesRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + PlatformRoles []string `json:"platformRoles" tfsdk:"platform_roles"` +} diff --git a/platform_properties_aws.go b/platform_properties_aws.go new file mode 100644 index 0000000..bbd8480 --- /dev/null +++ b/platform_properties_aws.go @@ -0,0 +1,14 @@ +package client + +type AwsPlatformProperties struct { + AwsTargetOrgUnitId string `json:"awsTargetOrgUnitId" tfsdk:"aws_target_org_unit_id"` + AwsEnrollAccount bool `json:"awsEnrollAccount" tfsdk:"aws_enroll_account"` + AwsLambdaArn *string `json:"awsLambdaArn" tfsdk:"aws_lambda_arn"` + AwsRoleMappings []AwsRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` +} + +type AwsRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + PlatformRole string `json:"platformRole" tfsdk:"platform_role"` + Policies []string `json:"policies" tfsdk:"policies"` +} diff --git a/platform_properties_azure.go b/platform_properties_azure.go new file mode 100644 index 0000000..a06c010 --- /dev/null +++ b/platform_properties_azure.go @@ -0,0 +1,17 @@ +package client + +type AzurePlatformProperties struct { + AzureRoleMappings []AzureRoleMapping `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` + AzureManagementGroupId string `json:"azureManagementGroupId" tfsdk:"azure_management_group_id"` +} + +type AzureRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureGroupSuffix string `json:"azureGroupSuffix" tfsdk:"azure_group_suffix"` + AzureRoleDefinitions []AzureRoleDefinition `json:"azureRoleDefinitions" tfsdk:"azure_role_definitions"` +} + +type AzureRoleDefinition struct { + AzureRoleDefinitionId string `json:"azureRoleDefinitionId" tfsdk:"azure_role_definition_id"` + AbacCondition *string `json:"abacCondition" tfsdk:"abac_condition"` +} diff --git a/platform_properties_azurerg.go b/platform_properties_azurerg.go new file mode 100644 index 0000000..dc97e8f --- /dev/null +++ b/platform_properties_azurerg.go @@ -0,0 +1,18 @@ +package client + +type AzureRgPlatformProperties struct { + AzureRgLocation string `json:"azureRgLocation" tfsdk:"azure_rg_location"` + AzureRgRoleMappings []AzureRgRoleMapping `json:"azureRgRoleMappings" tfsdk:"azure_rg_role_mappings"` + AzureFunction *AzureFunction `json:"azureFunction,omitempty" tfsdk:"azure_function"` +} + +type AzureRgRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureGroupSuffix string `json:"azureGroupSuffix" tfsdk:"azure_group_suffix"` + AzureRoleDefinitionIds []string `json:"azureRoleDefinitionIds" tfsdk:"azure_role_definition_ids"` +} + +type AzureFunction struct { + AzureFunctionUrl string `json:"azureFunctionUrl" tfsdk:"azure_function_url"` + AzureFunctionScope string `json:"azureFunctionScope" tfsdk:"azure_function_scope"` +} diff --git a/platform_properties_gcp.go b/platform_properties_gcp.go new file mode 100644 index 0000000..c8bb02b --- /dev/null +++ b/platform_properties_gcp.go @@ -0,0 +1,12 @@ +package client + +type GcpPlatformProperties struct { + GcpCloudFunctionUrl *string `json:"gcpCloudFunctionUrl,omitempty" tfsdk:"gcp_cloud_function_url"` + GcpFolderId *string `json:"gcpFolderId,omitempty" tfsdk:"gcp_folder_id"` + GcpRoleMappings []GcpRoleMapping `json:"gcpRoleMappings" tfsdk:"gcp_role_mappings"` +} + +type GcpRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + PlatformRoles []string `json:"platformRoles" tfsdk:"platform_roles"` +} diff --git a/platform_properties_kubernetes.go b/platform_properties_kubernetes.go new file mode 100644 index 0000000..b48c338 --- /dev/null +++ b/platform_properties_kubernetes.go @@ -0,0 +1,5 @@ +package client + +type KubernetesPlatformProperties struct { + KubernetesRoleMappings []KubernetesRoleMapping `json:"kubernetesRoleMappings" tfsdk:"kubernetes_role_mappings"` +} diff --git a/platform_properties_openshift.go b/platform_properties_openshift.go new file mode 100644 index 0000000..15d6752 --- /dev/null +++ b/platform_properties_openshift.go @@ -0,0 +1,5 @@ +package client + +type OpenShiftPlatformProperties struct { + OpenShiftTemplate *string `json:"openShiftTemplate,omitempty" tfsdk:"openshift_template"` +} diff --git a/project_binding.go b/project_binding.go index f704efc..9e7138e 100644 --- a/project_binding.go +++ b/project_binding.go @@ -22,10 +22,17 @@ type MeshProjectBindingMetadata struct { Name string `json:"name" tfsdk:"name"` } +// Deprecated: Use MeshProjectRoleRefV2 if possible. The convention is to also provide the `kind`, +// so this struct should only be used for meshobjects that violate our API conventions. type MeshProjectRoleRef struct { Name string `json:"name" tfsdk:"name"` } +type MeshProjectRoleRefV2 struct { + Name string `json:"name" tfsdk:"name"` + Kind string `json:"kind" tfsdk:"kind"` +} + type MeshProjectTargetRef struct { Name string `json:"name" tfsdk:"name"` OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` From 4782eb04cd0ecca5b12d509c0e55a3da43613cf8 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 19 Sep 2025 13:49:52 +0200 Subject: [PATCH 037/200] chore: fix capitalization: AWS -> Aws etc. --- landingzone.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/landingzone.go b/landingzone.go index ec4d097..f926510 100644 --- a/landingzone.go +++ b/landingzone.go @@ -40,11 +40,11 @@ type PlatformRef struct { type PlatformProperties struct { Type string `json:"type" tfsdk:"type"` - AWS *AwsPlatformProperties `json:"aws" tfsdk:"aws"` - AKS *AksPlatformProperties `json:"aks" tfsdk:"aks"` + Aws *AwsPlatformProperties `json:"aws" tfsdk:"aws"` + Aks *AksPlatformProperties `json:"aks" tfsdk:"aks"` Azure *AzurePlatformProperties `json:"azure" tfsdk:"azure"` - AzureRG *AzureRgPlatformProperties `json:"azurerg" tfsdk:"azurerg"` - GCP *GcpPlatformProperties `json:"gcp" tfsdk:"gcp"` + AzureRg *AzureRgPlatformProperties `json:"azurerg" tfsdk:"azurerg"` + Gcp *GcpPlatformProperties `json:"gcp" tfsdk:"gcp"` Kubernetes *KubernetesPlatformProperties `json:"kubernetes" tfsdk:"kubernetes"` OpenShift *OpenShiftPlatformProperties `json:"openshift" tfsdk:"openshift"` } From 409ebff63b0db286fc1bd0b92c5e6319aac9c884 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 19 Sep 2025 13:56:34 +0200 Subject: [PATCH 038/200] fix: add missing landing zone status --- landingzone.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/landingzone.go b/landingzone.go index f926510..5d19dd2 100644 --- a/landingzone.go +++ b/landingzone.go @@ -16,6 +16,7 @@ type MeshLandingZone struct { Kind string `json:"kind" tfsdk:"kind"` Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` + Status MeshLandingZoneStatus `json:"status" tfsdk:"status"` } type MeshLandingZoneMetadata struct { @@ -33,6 +34,11 @@ type MeshLandingZoneSpec struct { PlatformProperties *PlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` } +type MeshLandingZoneStatus struct { + Disabled string `json:"disabled" tfsdk:"disabled"` + Restricted string `json:"restricted" tfsdk:"restricted"` +} + type PlatformRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` Kind string `json:"kind" tfsdk:"kind"` From b6c1a50da38e982308a20ef48faf05113b0000c6 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 19 Sep 2025 13:57:55 +0200 Subject: [PATCH 039/200] chore: remove redundant MeshLandingZoneCreateMetadata --- landingzone.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/landingzone.go b/landingzone.go index 5d19dd2..a0e4b6f 100644 --- a/landingzone.go +++ b/landingzone.go @@ -56,13 +56,9 @@ type PlatformProperties struct { } type MeshLandingZoneCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Metadata MeshLandingZoneCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` -} -type MeshLandingZoneCreateMetadata struct { - Name string `json:"name" tfsdk:"name"` - Tags map[string][]string `json:"tags" tfsdk:"tags"` + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` } func (c *MeshStackProviderClient) urlForLandingZone(name string) *url.URL { From e918df69b900a8c85fb0f44b7047c486cfc93385 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 19 Sep 2025 14:38:44 +0200 Subject: [PATCH 040/200] fix: fix landing zone status --- landingzone.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landingzone.go b/landingzone.go index a0e4b6f..6f60d0b 100644 --- a/landingzone.go +++ b/landingzone.go @@ -35,8 +35,8 @@ type MeshLandingZoneSpec struct { } type MeshLandingZoneStatus struct { - Disabled string `json:"disabled" tfsdk:"disabled"` - Restricted string `json:"restricted" tfsdk:"restricted"` + Disabled bool `json:"disabled" tfsdk:"disabled"` + Restricted bool `json:"restricted" tfsdk:"restricted"` } type PlatformRef struct { From 0244cf4630ffe63a908667d0a3e16bbcd44efd7e Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Tue, 23 Sep 2025 11:13:34 +0200 Subject: [PATCH 041/200] feat: implement support for meshPlatforms CU-86c55h4xp --- client.go | 2 + platform.go | 503 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 505 insertions(+) create mode 100644 platform.go diff --git a/client.go b/client.go index 6d06efa..f9223fa 100644 --- a/client.go +++ b/client.go @@ -43,6 +43,7 @@ type endpoints struct { Tenants *url.URL `json:"meshtenants"` TagDefinitions *url.URL `json:"meshtagdefinitions"` LandingZones *url.URL `json:"meshlandingzones"` + Platforms *url.URL `json:"meshplatforms"` } type loginResponse struct { @@ -73,6 +74,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), LandingZones: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlandingzones"), + Platforms: rootUrl.JoinPath(apiMeshObjectsRoot, "meshplatforms"), } return client, nil diff --git a/platform.go b/platform.go new file mode 100644 index 0000000..57d5de7 --- /dev/null +++ b/platform.go @@ -0,0 +1,503 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_PLATFORM = "application/vnd.meshcloud.api.meshplatform.v2-preview.hal+json" + +type MeshPlatform struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshPlatformMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPlatformMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Uuid string `json:"uuid" tfsdk:"uuid"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` +} + +type MeshPlatformSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + Endpoint string `json:"endpoint" tfsdk:"endpoint"` + SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` + DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` + ContributingWorkspaces []string `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` + Availability PlatformAvailability `json:"availability" tfsdk:"availability"` + Config PlatformConfig `json:"config" tfsdk:"config"` +} + +type LocationRef struct { + Kind string `json:"kind" tfsdk:"kind"` + Name string `json:"name" tfsdk:"name"` +} + +type PlatformAvailability struct { + Restriction string `json:"restriction" tfsdk:"restriction"` + PublicationState string `json:"publicationState" tfsdk:"publication_state"` + RestrictedToWorkspaces []string `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` +} + +type PlatformConfig struct { + Type string `json:"type" tfsdk:"type"` + Aws *AwsPlatformConfig `json:"aws,omitempty" tfsdk:"aws"` + Aks *AksPlatformConfig `json:"aks,omitempty" tfsdk:"aks"` + Azure *AzurePlatformConfig `json:"azure,omitempty" tfsdk:"azure"` + AzureRg *AzureRgPlatformConfig `json:"azurerg,omitempty" tfsdk:"azurerg"` + Gcp *GcpPlatformConfig `json:"gcp,omitempty" tfsdk:"gcp"` + Kubernetes *KubernetesPlatformConfig `json:"kubernetes,omitempty" tfsdk:"kubernetes"` + OpenShift *OpenShiftPlatformConfig `json:"openshift,omitempty" tfsdk:"openshift"` +} + +type AwsPlatformConfig struct { + Region *string `json:"region,omitempty" tfsdk:"region"` + Replication *AwsReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` +} + +type AksPlatformConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` + Replication *AksReplicationConfig `json:"replication" tfsdk:"replication"` +} + +type AzurePlatformConfig struct { + EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` + Replication *AzureReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` +} + +type AzureRgPlatformConfig struct { + EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` + Replication *AzureRgReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` +} + +type GcpPlatformConfig struct { + Replication *GcpReplicationConfig `json:"replication" tfsdk:"replication"` +} + +type KubernetesPlatformConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` + Replication *KubernetesReplicationConfig `json:"replication" tfsdk:"replication"` +} + +type OpenShiftPlatformConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` + Replication *OpenShiftReplicationConfig `json:"replication" tfsdk:"replication"` +} + +type AzureRgReplicationConfig struct { + ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` + Subscription *string `json:"subscription,omitempty" tfsdk:"subscription"` + ResourceGroupNamePattern *string `json:"resourceGroupNamePattern,omitempty" tfsdk:"resource_group_name_pattern"` + UserGroupNamePattern *string `json:"userGroupNamePattern,omitempty" tfsdk:"user_group_name_pattern"` + B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` +} + +type GcpReplicationConfig struct { + ServiceAccountConfig *GcpServiceAccountConfig `json:"serviceAccountConfig,omitempty" tfsdk:"service_account_config"` + Domain *string `json:"domain,omitempty" tfsdk:"domain"` + CustomerId *string `json:"customerId,omitempty" tfsdk:"customer_id"` + GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` + ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` + ProjectIdPattern *string `json:"projectIdPattern,omitempty" tfsdk:"project_id_pattern"` + BillingAccountId *string `json:"billingAccountId,omitempty" tfsdk:"billing_account_id"` + UserLookupStrategy *string `json:"userLookupStrategy,omitempty" tfsdk:"user_lookup_strategy"` + GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings,omitempty" tfsdk:"gcp_role_mappings"` + AllowHierarchicalFolderAssignment *bool `json:"allowHierarchicalFolderAssignment,omitempty" tfsdk:"allow_hierarchical_folder_assignment"` + TenantTags *GcpTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` +} + +type GcpServiceAccountConfig struct { + ServiceAccountCredentialsConfig *GcpServiceAccountCredentialsConfig `json:"serviceAccountCredentialsConfig,omitempty" tfsdk:"service_account_credentials_config"` + ServiceAccountWorkloadIdentityConfig *GcpServiceAccountWorkloadIdentityConfig `json:"serviceAccountWorkloadIdentityConfig,omitempty" tfsdk:"service_account_workload_identity_config"` +} + +type GcpServiceAccountCredentialsConfig struct { + ServiceAccountCredentialsB64 *string `json:"serviceAccountCredentialsB64,omitempty" tfsdk:"service_account_credentials_b64"` +} + +type GcpServiceAccountWorkloadIdentityConfig struct { + Audience *string `json:"audience,omitempty" tfsdk:"audience"` + ServiceAccountEmail *string `json:"serviceAccountEmail,omitempty" tfsdk:"service_account_email"` +} + +type GcpTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []GcpTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type GcpTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type KubernetesReplicationConfig struct { + ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` +} + +type KubernetesClientConfig struct { + AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` +} + +type OpenShiftReplicationConfig struct { + ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` + ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` + EnableTemplateInstantiation *bool `json:"enableTemplateInstantiation,omitempty" tfsdk:"enable_template_instantiation"` + OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings,omitempty" tfsdk:"openshift_role_mappings"` + IdentityProviderName *string `json:"identityProviderName,omitempty" tfsdk:"identity_provider_name"` + TenantTags *OpenShiftTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` +} + +type OpenShiftClientConfig struct { + AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` +} + +type OpenShiftTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []OpenShiftTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type OpenShiftTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type AksReplicationConfig struct { + AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` + NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` + GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` + ServicePrincipal *ServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` + AksSubscriptionId *string `json:"aksSubscriptionId,omitempty" tfsdk:"aks_subscription_id"` + AksClusterName *string `json:"aksClusterName,omitempty" tfsdk:"aks_cluster_name"` + AksResourceGroup *string `json:"aksResourceGroup,omitempty" tfsdk:"aks_resource_group"` + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` + SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` + UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` +} + +type ServicePrincipalConfig struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + AuthType string `json:"authType" tfsdk:"auth_type"` + CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` + EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` + ObjectId string `json:"objectId" tfsdk:"object_id"` +} + +// Azure-specific service principal configurations +type AzureServicePrincipalConfig struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + AuthType string `json:"authType" tfsdk:"auth_type"` + CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` + ObjectId string `json:"objectId" tfsdk:"object_id"` +} + +type AzureSourceServicePrincipalConfig struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + AuthType string `json:"authType" tfsdk:"auth_type"` + CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` +} + +// AWS-specific replication configuration structures +type AwsReplicationConfig struct { + AccessConfig *AwsAccessConfig `json:"accessConfig,omitempty" tfsdk:"access_config"` + WaitForExternalAvm *bool `json:"waitForExternalAvm,omitempty" tfsdk:"wait_for_external_avm"` + AutomationAccountRole *string `json:"automationAccountRole,omitempty" tfsdk:"automation_account_role"` + AutomationAccountExternalId *string `json:"automationAccountExternalId,omitempty" tfsdk:"automation_account_external_id"` + AccountAccessRole *string `json:"accountAccessRole,omitempty" tfsdk:"account_access_role"` + AccountAliasPattern *string `json:"accountAliasPattern,omitempty" tfsdk:"account_alias_pattern"` + EnforceAccountAlias *bool `json:"enforceAccountAlias,omitempty" tfsdk:"enforce_account_alias"` + AccountEmailPattern *string `json:"accountEmailPattern,omitempty" tfsdk:"account_email_pattern"` + TenantTags *AwsTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + AwsSso *AwsSsoConfig `json:"awsSso,omitempty" tfsdk:"aws_sso"` + EnrollmentConfiguration *AwsEnrollmentConfiguration `json:"enrollmentConfiguration,omitempty" tfsdk:"enrollment_configuration"` + SelfDowngradeAccessRole *bool `json:"selfDowngradeAccessRole,omitempty" tfsdk:"self_downgrade_access_role"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + AllowHierarchicalOrganizationalUnitAssignment *bool `json:"allowHierarchicalOrganizationalUnitAssignment,omitempty" tfsdk:"allow_hierarchical_organizational_unit_assignment"` +} + +type AwsAccessConfig struct { + OrganizationRootAccountRole string `json:"organizationRootAccountRole" tfsdk:"organization_root_account_role"` + OrganizationRootAccountExternalId *string `json:"organizationRootAccountExternalId,omitempty" tfsdk:"organization_root_account_external_id"` + ServiceUserConfig *AwsServiceUserConfig `json:"serviceUserConfig,omitempty" tfsdk:"service_user_config"` + WorkloadIdentityConfig *AwsWorkloadIdentityConfig `json:"workloadIdentityConfig,omitempty" tfsdk:"workload_identity_config"` +} + +type AwsServiceUserConfig struct { + AccessKey string `json:"accessKey" tfsdk:"access_key"` + SecretKey *string `json:"secretKey,omitempty" tfsdk:"secret_key"` +} + +type AwsWorkloadIdentityConfig struct { + RoleArn string `json:"roleArn" tfsdk:"role_arn"` +} + +type AwsTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []AwsTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type AwsTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type AwsSsoConfig struct { + ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` + Arn string `json:"arn" tfsdk:"arn"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + SsoAccessToken *string `json:"ssoAccessToken,omitempty" tfsdk:"sso_access_token"` + AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` + SignInUrl *string `json:"signInUrl,omitempty" tfsdk:"sign_in_url"` +} + +type AwsSsoRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AwsRole string `json:"awsRole" tfsdk:"aws_role"` + PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` +} + +type GcpPlatformRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` +} + +type OpenShiftPlatformRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + OpenShiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` +} + +type AwsEnrollmentConfiguration struct { + ManagementAccountId string `json:"managementAccountId" tfsdk:"management_account_id"` + AccountFactoryProductId string `json:"accountFactoryProductId" tfsdk:"account_factory_product_id"` +} + +// Azure-specific replication configuration structures +type AzureReplicationConfig struct { + ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` + Provisioning *AzureProvisioning `json:"provisioning,omitempty" tfsdk:"provisioning"` + B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + SubscriptionNamePattern *string `json:"subscriptionNamePattern,omitempty" tfsdk:"subscription_name_pattern"` + GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` + BlueprintServicePrincipal *string `json:"blueprintServicePrincipal,omitempty" tfsdk:"blueprint_service_principal"` + BlueprintLocation *string `json:"blueprintLocation,omitempty" tfsdk:"blueprint_location"` + AzureRoleMappings []AzurePlatformRoleMapping `json:"azureRoleMappings,omitempty" tfsdk:"azure_role_mappings"` + TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` +} + +type AzureProvisioning struct { + SubscriptionOwnerObjectIds []string `json:"subscriptionOwnerObjectIds,omitempty" tfsdk:"subscription_owner_object_ids"` + EnterpriseEnrollment *AzureEnterpriseEnrollment `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` + CustomerAgreement *AzureCustomerAgreement `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` + PreProvisioned *AzurePreProvisioned `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` +} + +type AzureEnterpriseEnrollment struct { + EnrollmentAccountId string `json:"enrollmentAccountId" tfsdk:"enrollment_account_id"` + SubscriptionOfferType string `json:"subscriptionOfferType" tfsdk:"subscription_offer_type"` + UseLegacySubscriptionEnrollment *bool `json:"useLegacySubscriptionEnrollment,omitempty" tfsdk:"use_legacy_subscription_enrollment"` + SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` +} + +type AzureCustomerAgreement struct { + SourceServicePrincipal *AzureSourceServicePrincipalConfig `json:"sourceServicePrincipal,omitempty" tfsdk:"source_service_principal"` + DestinationEntraId string `json:"destinationEntraId" tfsdk:"destination_entra_id"` + SourceEntraTenant string `json:"sourceEntraTenant" tfsdk:"source_entra_tenant"` + BillingScope string `json:"billingScope" tfsdk:"billing_scope"` + SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` +} + +type AzurePreProvisioned struct { + UnusedSubscriptionNamePrefix string `json:"unusedSubscriptionNamePrefix" tfsdk:"unused_subscription_name_prefix"` +} + +type AzureB2bUserInvitation struct { + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` + SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` +} + +type AzurePlatformRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureRole AzurePlatformRoleDefinition `json:"azureRole" tfsdk:"azure_role"` +} + +type AzurePlatformRoleDefinition struct { + Alias string `json:"alias" tfsdk:"alias"` + Id string `json:"id" tfsdk:"id"` +} + +type AzureTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []AzureTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type AzureTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type MeshPlatformCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshPlatformCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPlatformCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshPlatformUpdate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshPlatformUpdateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPlatformUpdateMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Uuid string `json:"uuid" tfsdk:"uuid"` +} + +func (c *MeshStackProviderClient) urlForPlatform(uuid string) *url.URL { + return c.endpoints.Platforms.JoinPath(uuid) +} + +func (c *MeshStackProviderClient) ReadPlatform(uuid string) (*MeshPlatform, error) { + targetUrl := c.urlForPlatform(uuid) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil, nil // Not found is not an error + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var platform MeshPlatform + err = json.Unmarshal(data, &platform) + if err != nil { + return nil, err + } + return &platform, nil +} + +func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) (*MeshPlatform, error) { + payload, err := json.Marshal(platform) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Platforms.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) + req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdPlatform MeshPlatform + err = json.Unmarshal(data, &createdPlatform) + if err != nil { + return nil, err + } + return &createdPlatform, nil +} + +func (c *MeshStackProviderClient) DeletePlatform(uuid string) error { + targetUrl := c.urlForPlatform(uuid) + return c.deleteMeshObject(*targetUrl, 204) +} + +func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { + targetUrl := c.urlForPlatform(uuid) + + payload, err := json.Marshal(platform) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) + req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var updatedPlatform MeshPlatform + err = json.Unmarshal(data, &updatedPlatform) + if err != nil { + return nil, err + } + return &updatedPlatform, nil +} From d3fa716abaecf30a55dc46f84fea80a574138cfd Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 20 Oct 2025 15:03:41 +0200 Subject: [PATCH 042/200] fix: make landingzone info_link optional Fixes #60 --- landingzone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landingzone.go b/landingzone.go index 6f60d0b..20cca50 100644 --- a/landingzone.go +++ b/landingzone.go @@ -29,7 +29,7 @@ type MeshLandingZoneSpec struct { Description string `json:"description" tfsdk:"description"` AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` - InfoLink string `json:"infoLink" tfsdk:"info_link"` + InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` PlatformRef PlatformRef `json:"platformRef" tfsdk:"platform_ref"` PlatformProperties *PlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` } From 746f2c33683210e0517f06ee7e6da501153017c8 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Mon, 3 Nov 2025 12:13:36 +0100 Subject: [PATCH 043/200] chore: use new API endpoint for login and improve error message in case of login error --- client.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index f9223fa..608b778 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "encoding/json" "errors" "fmt" @@ -8,7 +9,6 @@ import ( "log" "net/http" "net/url" - "strings" "time" ) @@ -46,6 +46,11 @@ type endpoints struct { Platforms *url.URL `json:"meshplatforms"` } +type loginRequest struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` +} + type loginResponse struct { Token string `json:"access_token"` ExpireSec int `json:"expires_in"` @@ -86,20 +91,25 @@ func (c *MeshStackProviderClient) login() error { return err } - formData := url.Values{} - formData.Set("client_id", c.apiKey) - formData.Set("client_secret", c.apiSecret) - formData.Set("grant_type", "client_credentials") + loginRequest := loginRequest{ + ClientId: c.apiKey, + ClientSecret: c.apiSecret, + } + + payload, err := json.Marshal(loginRequest) + if err != nil { + return err + } - req, _ := http.NewRequest(http.MethodPost, loginPath, strings.NewReader(formData.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req, _ := http.NewRequest(http.MethodPost, loginPath, bytes.NewBuffer(payload)) + req.Header.Add("Content-Type", "application/json") res, err := c.httpClient.Do(req) if err != nil { return err } else if res.StatusCode != 200 { - return errors.New(ERROR_AUTHENTICATION_FAILURE) + return errors.New(fmt.Sprintf("Status %d: %s", res.StatusCode, ERROR_AUTHENTICATION_FAILURE)) } defer res.Body.Close() From 1ee35ca16f4afcd3b98b9eda20290c788da9e9c8 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 6 Nov 2025 11:38:27 +0100 Subject: [PATCH 044/200] feat: add quota definitions to meshPlatforms --- platform.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/platform.go b/platform.go index 57d5de7..5afdd3d 100644 --- a/platform.go +++ b/platform.go @@ -36,6 +36,17 @@ type MeshPlatformSpec struct { ContributingWorkspaces []string `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` Availability PlatformAvailability `json:"availability" tfsdk:"availability"` Config PlatformConfig `json:"config" tfsdk:"config"` + QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` +} + +type QuotaDefinition struct { + QuotaKey string `json:"quotaKey" tfsdk:"quota_key"` + MinValue int `json:"minValue" tfsdk:"min_value"` + MaxValue int `json:"maxValue" tfsdk:"max_value"` + Unit string `json:"unit" tfsdk:"unit"` + AutoApprovalThreshold int `json:"autoApprovalThreshold" tfsdk:"auto_approval_threshold"` + Description string `json:"description" tfsdk:"description"` + Label string `json:"label" tfsdk:"label"` } type LocationRef struct { From 5460edfd01ff2d22e28b8a8ce27b8a1129fdda3c Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 12 Nov 2025 12:05:08 +0100 Subject: [PATCH 045/200] feat: add metering config to platform --- platform.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/platform.go b/platform.go index 5afdd3d..8251a76 100644 --- a/platform.go +++ b/platform.go @@ -100,12 +100,14 @@ type KubernetesPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` Replication *KubernetesReplicationConfig `json:"replication" tfsdk:"replication"` + Metering *KubernetesMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type OpenShiftPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` Replication *OpenShiftReplicationConfig `json:"replication" tfsdk:"replication"` + Metering *OpenShiftMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type AzureRgReplicationConfig struct { @@ -169,6 +171,11 @@ type KubernetesClientConfig struct { AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` } +type KubernetesMeteringConfig struct { + ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} + type OpenShiftReplicationConfig struct { ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` @@ -183,6 +190,11 @@ type OpenShiftClientConfig struct { AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` } +type OpenShiftMeteringConfig struct { + ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} + type OpenShiftTenantTags struct { NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` TagMappers []OpenShiftTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` @@ -394,6 +406,11 @@ type MeshPlatformUpdateMetadata struct { Uuid string `json:"uuid" tfsdk:"uuid"` } +type MeshPlatformMeteringProcessingConfig struct { + CompactTimelinesAfterDays int64 `json:"compactTimelinesAfterDays" tfsdk:"compact_timelines_after_days"` + DeleteRawDataAfterDays int64 `json:"deleteRawDataAfterDays" tfsdk:"delete_raw_data_after_days"` +} + func (c *MeshStackProviderClient) urlForPlatform(uuid string) *url.URL { return c.endpoints.Platforms.JoinPath(uuid) } From 8945872c2d181b30600bb7d551ea4b6dbe78e707 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 13 Nov 2025 09:10:46 +0100 Subject: [PATCH 046/200] feature: metering config for aks platforms --- platform.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/platform.go b/platform.go index 8251a76..49e046a 100644 --- a/platform.go +++ b/platform.go @@ -80,6 +80,7 @@ type AksPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` Replication *AksReplicationConfig `json:"replication" tfsdk:"replication"` + Metering *AksMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type AzurePlatformConfig struct { @@ -227,6 +228,11 @@ type ServicePrincipalConfig struct { ObjectId string `json:"objectId" tfsdk:"object_id"` } +type AksMeteringConfig struct { + ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} + // Azure-specific service principal configurations type AzureServicePrincipalConfig struct { ClientId string `json:"clientId" tfsdk:"client_id"` From 1bf89447515d4b9a2fbd809f24db35e866a4d38a Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 13 Nov 2025 09:16:02 +0100 Subject: [PATCH 047/200] refactor: separate platform configs by platform --- platform.go | 318 ---------------------------------- platform_config_aks.go | 35 ++++ platform_config_aws.go | 69 ++++++++ platform_config_azure.go | 86 +++++++++ platform_config_azurerg.go | 19 ++ platform_config_gcp.go | 49 ++++++ platform_config_kubernetes.go | 22 +++ platform_config_openshift.go | 42 +++++ 8 files changed, 322 insertions(+), 318 deletions(-) create mode 100644 platform_config_aks.go create mode 100644 platform_config_aws.go create mode 100644 platform_config_azure.go create mode 100644 platform_config_azurerg.go create mode 100644 platform_config_gcp.go create mode 100644 platform_config_kubernetes.go create mode 100644 platform_config_openshift.go diff --git a/platform.go b/platform.go index 49e046a..bc40515 100644 --- a/platform.go +++ b/platform.go @@ -71,324 +71,6 @@ type PlatformConfig struct { OpenShift *OpenShiftPlatformConfig `json:"openshift,omitempty" tfsdk:"openshift"` } -type AwsPlatformConfig struct { - Region *string `json:"region,omitempty" tfsdk:"region"` - Replication *AwsReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` -} - -type AksPlatformConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` - Replication *AksReplicationConfig `json:"replication" tfsdk:"replication"` - Metering *AksMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` -} - -type AzurePlatformConfig struct { - EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` - Replication *AzureReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` -} - -type AzureRgPlatformConfig struct { - EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` - Replication *AzureRgReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` -} - -type GcpPlatformConfig struct { - Replication *GcpReplicationConfig `json:"replication" tfsdk:"replication"` -} - -type KubernetesPlatformConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` - Replication *KubernetesReplicationConfig `json:"replication" tfsdk:"replication"` - Metering *KubernetesMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` -} - -type OpenShiftPlatformConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` - Replication *OpenShiftReplicationConfig `json:"replication" tfsdk:"replication"` - Metering *OpenShiftMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` -} - -type AzureRgReplicationConfig struct { - ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` - Subscription *string `json:"subscription,omitempty" tfsdk:"subscription"` - ResourceGroupNamePattern *string `json:"resourceGroupNamePattern,omitempty" tfsdk:"resource_group_name_pattern"` - UserGroupNamePattern *string `json:"userGroupNamePattern,omitempty" tfsdk:"user_group_name_pattern"` - B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` - TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` - AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` -} - -type GcpReplicationConfig struct { - ServiceAccountConfig *GcpServiceAccountConfig `json:"serviceAccountConfig,omitempty" tfsdk:"service_account_config"` - Domain *string `json:"domain,omitempty" tfsdk:"domain"` - CustomerId *string `json:"customerId,omitempty" tfsdk:"customer_id"` - GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` - ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` - ProjectIdPattern *string `json:"projectIdPattern,omitempty" tfsdk:"project_id_pattern"` - BillingAccountId *string `json:"billingAccountId,omitempty" tfsdk:"billing_account_id"` - UserLookupStrategy *string `json:"userLookupStrategy,omitempty" tfsdk:"user_lookup_strategy"` - GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings,omitempty" tfsdk:"gcp_role_mappings"` - AllowHierarchicalFolderAssignment *bool `json:"allowHierarchicalFolderAssignment,omitempty" tfsdk:"allow_hierarchical_folder_assignment"` - TenantTags *GcpTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` -} - -type GcpServiceAccountConfig struct { - ServiceAccountCredentialsConfig *GcpServiceAccountCredentialsConfig `json:"serviceAccountCredentialsConfig,omitempty" tfsdk:"service_account_credentials_config"` - ServiceAccountWorkloadIdentityConfig *GcpServiceAccountWorkloadIdentityConfig `json:"serviceAccountWorkloadIdentityConfig,omitempty" tfsdk:"service_account_workload_identity_config"` -} - -type GcpServiceAccountCredentialsConfig struct { - ServiceAccountCredentialsB64 *string `json:"serviceAccountCredentialsB64,omitempty" tfsdk:"service_account_credentials_b64"` -} - -type GcpServiceAccountWorkloadIdentityConfig struct { - Audience *string `json:"audience,omitempty" tfsdk:"audience"` - ServiceAccountEmail *string `json:"serviceAccountEmail,omitempty" tfsdk:"service_account_email"` -} - -type GcpTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []GcpTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type GcpTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - -type KubernetesReplicationConfig struct { - ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` -} - -type KubernetesClientConfig struct { - AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` -} - -type KubernetesMeteringConfig struct { - ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` -} - -type OpenShiftReplicationConfig struct { - ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` - ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` - EnableTemplateInstantiation *bool `json:"enableTemplateInstantiation,omitempty" tfsdk:"enable_template_instantiation"` - OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings,omitempty" tfsdk:"openshift_role_mappings"` - IdentityProviderName *string `json:"identityProviderName,omitempty" tfsdk:"identity_provider_name"` - TenantTags *OpenShiftTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` -} - -type OpenShiftClientConfig struct { - AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` -} - -type OpenShiftMeteringConfig struct { - ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` -} - -type OpenShiftTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []OpenShiftTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type OpenShiftTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - -type AksReplicationConfig struct { - AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` - NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` - GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` - ServicePrincipal *ServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` - AksSubscriptionId *string `json:"aksSubscriptionId,omitempty" tfsdk:"aks_subscription_id"` - AksClusterName *string `json:"aksClusterName,omitempty" tfsdk:"aks_cluster_name"` - AksResourceGroup *string `json:"aksResourceGroup,omitempty" tfsdk:"aks_resource_group"` - RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` - SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` - UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` -} - -type ServicePrincipalConfig struct { - ClientId string `json:"clientId" tfsdk:"client_id"` - AuthType string `json:"authType" tfsdk:"auth_type"` - CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` - EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` - ObjectId string `json:"objectId" tfsdk:"object_id"` -} - -type AksMeteringConfig struct { - ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` -} - -// Azure-specific service principal configurations -type AzureServicePrincipalConfig struct { - ClientId string `json:"clientId" tfsdk:"client_id"` - AuthType string `json:"authType" tfsdk:"auth_type"` - CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` - ObjectId string `json:"objectId" tfsdk:"object_id"` -} - -type AzureSourceServicePrincipalConfig struct { - ClientId string `json:"clientId" tfsdk:"client_id"` - AuthType string `json:"authType" tfsdk:"auth_type"` - CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` -} - -// AWS-specific replication configuration structures -type AwsReplicationConfig struct { - AccessConfig *AwsAccessConfig `json:"accessConfig,omitempty" tfsdk:"access_config"` - WaitForExternalAvm *bool `json:"waitForExternalAvm,omitempty" tfsdk:"wait_for_external_avm"` - AutomationAccountRole *string `json:"automationAccountRole,omitempty" tfsdk:"automation_account_role"` - AutomationAccountExternalId *string `json:"automationAccountExternalId,omitempty" tfsdk:"automation_account_external_id"` - AccountAccessRole *string `json:"accountAccessRole,omitempty" tfsdk:"account_access_role"` - AccountAliasPattern *string `json:"accountAliasPattern,omitempty" tfsdk:"account_alias_pattern"` - EnforceAccountAlias *bool `json:"enforceAccountAlias,omitempty" tfsdk:"enforce_account_alias"` - AccountEmailPattern *string `json:"accountEmailPattern,omitempty" tfsdk:"account_email_pattern"` - TenantTags *AwsTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - AwsSso *AwsSsoConfig `json:"awsSso,omitempty" tfsdk:"aws_sso"` - EnrollmentConfiguration *AwsEnrollmentConfiguration `json:"enrollmentConfiguration,omitempty" tfsdk:"enrollment_configuration"` - SelfDowngradeAccessRole *bool `json:"selfDowngradeAccessRole,omitempty" tfsdk:"self_downgrade_access_role"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` - AllowHierarchicalOrganizationalUnitAssignment *bool `json:"allowHierarchicalOrganizationalUnitAssignment,omitempty" tfsdk:"allow_hierarchical_organizational_unit_assignment"` -} - -type AwsAccessConfig struct { - OrganizationRootAccountRole string `json:"organizationRootAccountRole" tfsdk:"organization_root_account_role"` - OrganizationRootAccountExternalId *string `json:"organizationRootAccountExternalId,omitempty" tfsdk:"organization_root_account_external_id"` - ServiceUserConfig *AwsServiceUserConfig `json:"serviceUserConfig,omitempty" tfsdk:"service_user_config"` - WorkloadIdentityConfig *AwsWorkloadIdentityConfig `json:"workloadIdentityConfig,omitempty" tfsdk:"workload_identity_config"` -} - -type AwsServiceUserConfig struct { - AccessKey string `json:"accessKey" tfsdk:"access_key"` - SecretKey *string `json:"secretKey,omitempty" tfsdk:"secret_key"` -} - -type AwsWorkloadIdentityConfig struct { - RoleArn string `json:"roleArn" tfsdk:"role_arn"` -} - -type AwsTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []AwsTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type AwsTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - -type AwsSsoConfig struct { - ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` - Arn string `json:"arn" tfsdk:"arn"` - GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - SsoAccessToken *string `json:"ssoAccessToken,omitempty" tfsdk:"sso_access_token"` - AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` - SignInUrl *string `json:"signInUrl,omitempty" tfsdk:"sign_in_url"` -} - -type AwsSsoRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AwsRole string `json:"awsRole" tfsdk:"aws_role"` - PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` -} - -type GcpPlatformRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` -} - -type OpenShiftPlatformRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - OpenShiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` -} - -type AwsEnrollmentConfiguration struct { - ManagementAccountId string `json:"managementAccountId" tfsdk:"management_account_id"` - AccountFactoryProductId string `json:"accountFactoryProductId" tfsdk:"account_factory_product_id"` -} - -// Azure-specific replication configuration structures -type AzureReplicationConfig struct { - ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` - Provisioning *AzureProvisioning `json:"provisioning,omitempty" tfsdk:"provisioning"` - B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - SubscriptionNamePattern *string `json:"subscriptionNamePattern,omitempty" tfsdk:"subscription_name_pattern"` - GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` - BlueprintServicePrincipal *string `json:"blueprintServicePrincipal,omitempty" tfsdk:"blueprint_service_principal"` - BlueprintLocation *string `json:"blueprintLocation,omitempty" tfsdk:"blueprint_location"` - AzureRoleMappings []AzurePlatformRoleMapping `json:"azureRoleMappings,omitempty" tfsdk:"azure_role_mappings"` - TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` - AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` -} - -type AzureProvisioning struct { - SubscriptionOwnerObjectIds []string `json:"subscriptionOwnerObjectIds,omitempty" tfsdk:"subscription_owner_object_ids"` - EnterpriseEnrollment *AzureEnterpriseEnrollment `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` - CustomerAgreement *AzureCustomerAgreement `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` - PreProvisioned *AzurePreProvisioned `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` -} - -type AzureEnterpriseEnrollment struct { - EnrollmentAccountId string `json:"enrollmentAccountId" tfsdk:"enrollment_account_id"` - SubscriptionOfferType string `json:"subscriptionOfferType" tfsdk:"subscription_offer_type"` - UseLegacySubscriptionEnrollment *bool `json:"useLegacySubscriptionEnrollment,omitempty" tfsdk:"use_legacy_subscription_enrollment"` - SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` -} - -type AzureCustomerAgreement struct { - SourceServicePrincipal *AzureSourceServicePrincipalConfig `json:"sourceServicePrincipal,omitempty" tfsdk:"source_service_principal"` - DestinationEntraId string `json:"destinationEntraId" tfsdk:"destination_entra_id"` - SourceEntraTenant string `json:"sourceEntraTenant" tfsdk:"source_entra_tenant"` - BillingScope string `json:"billingScope" tfsdk:"billing_scope"` - SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` -} - -type AzurePreProvisioned struct { - UnusedSubscriptionNamePrefix string `json:"unusedSubscriptionNamePrefix" tfsdk:"unused_subscription_name_prefix"` -} - -type AzureB2bUserInvitation struct { - RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` - SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` -} - -type AzurePlatformRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AzureRole AzurePlatformRoleDefinition `json:"azureRole" tfsdk:"azure_role"` -} - -type AzurePlatformRoleDefinition struct { - Alias string `json:"alias" tfsdk:"alias"` - Id string `json:"id" tfsdk:"id"` -} - -type AzureTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []AzureTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type AzureTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - type MeshPlatformCreate struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Metadata MeshPlatformCreateMetadata `json:"metadata" tfsdk:"metadata"` diff --git a/platform_config_aks.go b/platform_config_aks.go new file mode 100644 index 0000000..c02162b --- /dev/null +++ b/platform_config_aks.go @@ -0,0 +1,35 @@ +package client + +type AksPlatformConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` + Replication *AksReplicationConfig `json:"replication" tfsdk:"replication"` + Metering *AksMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` +} + +type AksReplicationConfig struct { + AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` + NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` + GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` + ServicePrincipal *ServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` + AksSubscriptionId *string `json:"aksSubscriptionId,omitempty" tfsdk:"aks_subscription_id"` + AksClusterName *string `json:"aksClusterName,omitempty" tfsdk:"aks_cluster_name"` + AksResourceGroup *string `json:"aksResourceGroup,omitempty" tfsdk:"aks_resource_group"` + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` + SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` + UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` +} + +type ServicePrincipalConfig struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + AuthType string `json:"authType" tfsdk:"auth_type"` + CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` + EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` + ObjectId string `json:"objectId" tfsdk:"object_id"` +} + +type AksMeteringConfig struct { + ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} diff --git a/platform_config_aws.go b/platform_config_aws.go new file mode 100644 index 0000000..b109a4c --- /dev/null +++ b/platform_config_aws.go @@ -0,0 +1,69 @@ +package client + +type AwsPlatformConfig struct { + Region *string `json:"region,omitempty" tfsdk:"region"` + Replication *AwsReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` +} + +type AwsReplicationConfig struct { + AccessConfig *AwsAccessConfig `json:"accessConfig,omitempty" tfsdk:"access_config"` + WaitForExternalAvm *bool `json:"waitForExternalAvm,omitempty" tfsdk:"wait_for_external_avm"` + AutomationAccountRole *string `json:"automationAccountRole,omitempty" tfsdk:"automation_account_role"` + AutomationAccountExternalId *string `json:"automationAccountExternalId,omitempty" tfsdk:"automation_account_external_id"` + AccountAccessRole *string `json:"accountAccessRole,omitempty" tfsdk:"account_access_role"` + AccountAliasPattern *string `json:"accountAliasPattern,omitempty" tfsdk:"account_alias_pattern"` + EnforceAccountAlias *bool `json:"enforceAccountAlias,omitempty" tfsdk:"enforce_account_alias"` + AccountEmailPattern *string `json:"accountEmailPattern,omitempty" tfsdk:"account_email_pattern"` + TenantTags *AwsTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + AwsSso *AwsSsoConfig `json:"awsSso,omitempty" tfsdk:"aws_sso"` + EnrollmentConfiguration *AwsEnrollmentConfiguration `json:"enrollmentConfiguration,omitempty" tfsdk:"enrollment_configuration"` + SelfDowngradeAccessRole *bool `json:"selfDowngradeAccessRole,omitempty" tfsdk:"self_downgrade_access_role"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + AllowHierarchicalOrganizationalUnitAssignment *bool `json:"allowHierarchicalOrganizationalUnitAssignment,omitempty" tfsdk:"allow_hierarchical_organizational_unit_assignment"` +} + +type AwsAccessConfig struct { + OrganizationRootAccountRole string `json:"organizationRootAccountRole" tfsdk:"organization_root_account_role"` + OrganizationRootAccountExternalId *string `json:"organizationRootAccountExternalId,omitempty" tfsdk:"organization_root_account_external_id"` + ServiceUserConfig *AwsServiceUserConfig `json:"serviceUserConfig,omitempty" tfsdk:"service_user_config"` + WorkloadIdentityConfig *AwsWorkloadIdentityConfig `json:"workloadIdentityConfig,omitempty" tfsdk:"workload_identity_config"` +} + +type AwsServiceUserConfig struct { + AccessKey string `json:"accessKey" tfsdk:"access_key"` + SecretKey *string `json:"secretKey,omitempty" tfsdk:"secret_key"` +} + +type AwsWorkloadIdentityConfig struct { + RoleArn string `json:"roleArn" tfsdk:"role_arn"` +} + +type AwsTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []AwsTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type AwsTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type AwsSsoConfig struct { + ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` + Arn string `json:"arn" tfsdk:"arn"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + SsoAccessToken *string `json:"ssoAccessToken,omitempty" tfsdk:"sso_access_token"` + AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` + SignInUrl *string `json:"signInUrl,omitempty" tfsdk:"sign_in_url"` +} + +type AwsSsoRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AwsRole string `json:"awsRole" tfsdk:"aws_role"` + PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` +} + +type AwsEnrollmentConfiguration struct { + ManagementAccountId string `json:"managementAccountId" tfsdk:"management_account_id"` + AccountFactoryProductId string `json:"accountFactoryProductId" tfsdk:"account_factory_product_id"` +} diff --git a/platform_config_azure.go b/platform_config_azure.go new file mode 100644 index 0000000..0549326 --- /dev/null +++ b/platform_config_azure.go @@ -0,0 +1,86 @@ +package client + +type AzurePlatformConfig struct { + EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` + Replication *AzureReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` +} + +type AzureReplicationConfig struct { + ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` + Provisioning *AzureProvisioning `json:"provisioning,omitempty" tfsdk:"provisioning"` + B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + SubscriptionNamePattern *string `json:"subscriptionNamePattern,omitempty" tfsdk:"subscription_name_pattern"` + GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` + BlueprintServicePrincipal *string `json:"blueprintServicePrincipal,omitempty" tfsdk:"blueprint_service_principal"` + BlueprintLocation *string `json:"blueprintLocation,omitempty" tfsdk:"blueprint_location"` + AzureRoleMappings []AzurePlatformRoleMapping `json:"azureRoleMappings,omitempty" tfsdk:"azure_role_mappings"` + TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` +} + +type AzureServicePrincipalConfig struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + AuthType string `json:"authType" tfsdk:"auth_type"` + CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` + ObjectId string `json:"objectId" tfsdk:"object_id"` +} + +type AzureSourceServicePrincipalConfig struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + AuthType string `json:"authType" tfsdk:"auth_type"` + CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` +} + +type AzureProvisioning struct { + SubscriptionOwnerObjectIds []string `json:"subscriptionOwnerObjectIds,omitempty" tfsdk:"subscription_owner_object_ids"` + EnterpriseEnrollment *AzureEnterpriseEnrollment `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` + CustomerAgreement *AzureCustomerAgreement `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` + PreProvisioned *AzurePreProvisioned `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` +} + +type AzureEnterpriseEnrollment struct { + EnrollmentAccountId string `json:"enrollmentAccountId" tfsdk:"enrollment_account_id"` + SubscriptionOfferType string `json:"subscriptionOfferType" tfsdk:"subscription_offer_type"` + UseLegacySubscriptionEnrollment *bool `json:"useLegacySubscriptionEnrollment,omitempty" tfsdk:"use_legacy_subscription_enrollment"` + SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` +} + +type AzureCustomerAgreement struct { + SourceServicePrincipal *AzureSourceServicePrincipalConfig `json:"sourceServicePrincipal,omitempty" tfsdk:"source_service_principal"` + DestinationEntraId string `json:"destinationEntraId" tfsdk:"destination_entra_id"` + SourceEntraTenant string `json:"sourceEntraTenant" tfsdk:"source_entra_tenant"` + BillingScope string `json:"billingScope" tfsdk:"billing_scope"` + SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` +} + +type AzurePreProvisioned struct { + UnusedSubscriptionNamePrefix string `json:"unusedSubscriptionNamePrefix" tfsdk:"unused_subscription_name_prefix"` +} + +type AzureB2bUserInvitation struct { + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` + SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` +} + +type AzurePlatformRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureRole AzurePlatformRoleDefinition `json:"azureRole" tfsdk:"azure_role"` +} + +type AzurePlatformRoleDefinition struct { + Alias string `json:"alias" tfsdk:"alias"` + Id string `json:"id" tfsdk:"id"` +} + +type AzureTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []AzureTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type AzureTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} diff --git a/platform_config_azurerg.go b/platform_config_azurerg.go new file mode 100644 index 0000000..47f585a --- /dev/null +++ b/platform_config_azurerg.go @@ -0,0 +1,19 @@ +package client + +type AzureRgPlatformConfig struct { + EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` + Replication *AzureRgReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` +} + +type AzureRgReplicationConfig struct { + ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` + Subscription *string `json:"subscription,omitempty" tfsdk:"subscription"` + ResourceGroupNamePattern *string `json:"resourceGroupNamePattern,omitempty" tfsdk:"resource_group_name_pattern"` + UserGroupNamePattern *string `json:"userGroupNamePattern,omitempty" tfsdk:"user_group_name_pattern"` + B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` +} diff --git a/platform_config_gcp.go b/platform_config_gcp.go new file mode 100644 index 0000000..0ee137b --- /dev/null +++ b/platform_config_gcp.go @@ -0,0 +1,49 @@ +package client + +type GcpPlatformConfig struct { + Replication *GcpReplicationConfig `json:"replication" tfsdk:"replication"` +} + +type GcpReplicationConfig struct { + ServiceAccountConfig *GcpServiceAccountConfig `json:"serviceAccountConfig,omitempty" tfsdk:"service_account_config"` + Domain *string `json:"domain,omitempty" tfsdk:"domain"` + CustomerId *string `json:"customerId,omitempty" tfsdk:"customer_id"` + GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` + ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` + ProjectIdPattern *string `json:"projectIdPattern,omitempty" tfsdk:"project_id_pattern"` + BillingAccountId *string `json:"billingAccountId,omitempty" tfsdk:"billing_account_id"` + UserLookupStrategy *string `json:"userLookupStrategy,omitempty" tfsdk:"user_lookup_strategy"` + GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings,omitempty" tfsdk:"gcp_role_mappings"` + AllowHierarchicalFolderAssignment *bool `json:"allowHierarchicalFolderAssignment,omitempty" tfsdk:"allow_hierarchical_folder_assignment"` + TenantTags *GcpTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` +} + +type GcpServiceAccountConfig struct { + ServiceAccountCredentialsConfig *GcpServiceAccountCredentialsConfig `json:"serviceAccountCredentialsConfig,omitempty" tfsdk:"service_account_credentials_config"` + ServiceAccountWorkloadIdentityConfig *GcpServiceAccountWorkloadIdentityConfig `json:"serviceAccountWorkloadIdentityConfig,omitempty" tfsdk:"service_account_workload_identity_config"` +} + +type GcpServiceAccountCredentialsConfig struct { + ServiceAccountCredentialsB64 *string `json:"serviceAccountCredentialsB64,omitempty" tfsdk:"service_account_credentials_b64"` +} + +type GcpServiceAccountWorkloadIdentityConfig struct { + Audience *string `json:"audience,omitempty" tfsdk:"audience"` + ServiceAccountEmail *string `json:"serviceAccountEmail,omitempty" tfsdk:"service_account_email"` +} + +type GcpTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []GcpTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type GcpTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type GcpPlatformRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` +} diff --git a/platform_config_kubernetes.go b/platform_config_kubernetes.go new file mode 100644 index 0000000..8ea5768 --- /dev/null +++ b/platform_config_kubernetes.go @@ -0,0 +1,22 @@ +package client + +type KubernetesPlatformConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` + Replication *KubernetesReplicationConfig `json:"replication" tfsdk:"replication"` + Metering *KubernetesMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` +} + +type KubernetesReplicationConfig struct { + ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` +} + +type KubernetesClientConfig struct { + AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` +} + +type KubernetesMeteringConfig struct { + ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} diff --git a/platform_config_openshift.go b/platform_config_openshift.go new file mode 100644 index 0000000..80f5ca6 --- /dev/null +++ b/platform_config_openshift.go @@ -0,0 +1,42 @@ +package client + +type OpenShiftPlatformConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` + Replication *OpenShiftReplicationConfig `json:"replication" tfsdk:"replication"` + Metering *OpenShiftMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` +} + +type OpenShiftReplicationConfig struct { + ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` + ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` + EnableTemplateInstantiation *bool `json:"enableTemplateInstantiation,omitempty" tfsdk:"enable_template_instantiation"` + OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings,omitempty" tfsdk:"openshift_role_mappings"` + IdentityProviderName *string `json:"identityProviderName,omitempty" tfsdk:"identity_provider_name"` + TenantTags *OpenShiftTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` +} + +type OpenShiftClientConfig struct { + AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` +} + +type OpenShiftMeteringConfig struct { + ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} + +type OpenShiftTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []OpenShiftTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type OpenShiftTagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + +type OpenShiftPlatformRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + OpenShiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` +} From 470415ddbd80fe122f6b1572951338d5351b9c09 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 13 Nov 2025 10:05:38 +0100 Subject: [PATCH 048/200] fix: correctly model nullable platform config fields --- platform_config_aks.go | 26 ++++++------ platform_config_aws.go | 9 ++++ platform_config_azure.go | 80 +++++++++++++++++++---------------- platform_config_azurerg.go | 18 ++++---- platform_config_gcp.go | 42 +++++++++++------- platform_config_kubernetes.go | 10 ++--- platform_properties_azure.go | 6 +-- 7 files changed, 109 insertions(+), 82 deletions(-) diff --git a/platform_config_aks.go b/platform_config_aks.go index c02162b..66f251f 100644 --- a/platform_config_aks.go +++ b/platform_config_aks.go @@ -8,17 +8,17 @@ type AksPlatformConfig struct { } type AksReplicationConfig struct { - AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` - NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` - GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` - ServicePrincipal *ServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` - AksSubscriptionId *string `json:"aksSubscriptionId,omitempty" tfsdk:"aks_subscription_id"` - AksClusterName *string `json:"aksClusterName,omitempty" tfsdk:"aks_cluster_name"` - AksResourceGroup *string `json:"aksResourceGroup,omitempty" tfsdk:"aks_resource_group"` - RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` - SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` - UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AccessToken string `json:"accessToken" tfsdk:"access_token"` + NamespaceNamePattern string `json:"namespaceNamePattern" tfsdk:"namespace_name_pattern"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + ServicePrincipal ServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + AksSubscriptionId string `json:"aksSubscriptionId" tfsdk:"aks_subscription_id"` + AksClusterName string `json:"aksClusterName" tfsdk:"aks_cluster_name"` + AksResourceGroup string `json:"aksResourceGroup" tfsdk:"aks_resource_group"` + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` + SendAzureInvitationMail bool `json:"sendAzureInvitationMail" tfsdk:"send_azure_invitation_mail"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` } type ServicePrincipalConfig struct { @@ -30,6 +30,6 @@ type ServicePrincipalConfig struct { } type AksMeteringConfig struct { - ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` + Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` } diff --git a/platform_config_aws.go b/platform_config_aws.go index b109a4c..a17a4a2 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -3,6 +3,7 @@ package client type AwsPlatformConfig struct { Region *string `json:"region,omitempty" tfsdk:"region"` Replication *AwsReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` + Metering *AwsMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type AwsReplicationConfig struct { @@ -67,3 +68,11 @@ type AwsEnrollmentConfiguration struct { ManagementAccountId string `json:"managementAccountId" tfsdk:"management_account_id"` AccountFactoryProductId string `json:"accountFactoryProductId" tfsdk:"account_factory_product_id"` } + +type AwsMeteringConfig struct { + AccessConfig AwsAccessConfig `json:"accessConfig" tfsdk:"access_config"` + Filter string `json:"filter" tfsdk:"filter"` + ReservedInstanceFairChargeback bool `json:"reservedInstanceFairChargeback" tfsdk:"reserved_instance_fair_chargeback"` + SavingsPlanFairChargeback bool `json:"savingsPlanFairChargeback" tfsdk:"savings_plan_fair_chargeback"` + Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` +} diff --git a/platform_config_azure.go b/platform_config_azure.go index 0549326..33516d0 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -1,24 +1,25 @@ package client type AzurePlatformConfig struct { - EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` + EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` Replication *AzureReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` + Metering *AzureMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type AzureReplicationConfig struct { - ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` - Provisioning *AzureProvisioning `json:"provisioning,omitempty" tfsdk:"provisioning"` - B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - SubscriptionNamePattern *string `json:"subscriptionNamePattern,omitempty" tfsdk:"subscription_name_pattern"` - GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` - BlueprintServicePrincipal *string `json:"blueprintServicePrincipal,omitempty" tfsdk:"blueprint_service_principal"` - BlueprintLocation *string `json:"blueprintLocation,omitempty" tfsdk:"blueprint_location"` - AzureRoleMappings []AzurePlatformRoleMapping `json:"azureRoleMappings,omitempty" tfsdk:"azure_role_mappings"` - TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` - AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` + ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + Provisioning *AzureSubscriptionProvisioningConfig `json:"provisioning,omitempty" tfsdk:"provisioning"` + B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + SubscriptionNamePattern string `json:"subscriptionNamePattern" tfsdk:"subscription_name_pattern"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + BlueprintServicePrincipal string `json:"blueprintServicePrincipal" tfsdk:"blueprint_service_principal"` + BlueprintLocation string `json:"blueprintLocation" tfsdk:"blueprint_location"` + AzureRoleMappings []AzureRoleMapping `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` + TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` } type AzureServicePrincipalConfig struct { @@ -28,49 +29,49 @@ type AzureServicePrincipalConfig struct { ObjectId string `json:"objectId" tfsdk:"object_id"` } -type AzureSourceServicePrincipalConfig struct { +type AzureGraphApiCredentials struct { ClientId string `json:"clientId" tfsdk:"client_id"` AuthType string `json:"authType" tfsdk:"auth_type"` CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` } -type AzureProvisioning struct { - SubscriptionOwnerObjectIds []string `json:"subscriptionOwnerObjectIds,omitempty" tfsdk:"subscription_owner_object_ids"` - EnterpriseEnrollment *AzureEnterpriseEnrollment `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` - CustomerAgreement *AzureCustomerAgreement `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` - PreProvisioned *AzurePreProvisioned `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` +type AzureSubscriptionProvisioningConfig struct { + SubscriptionOwnerObjectIds []string `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` + EnterpriseEnrollment *AzureEnterpriseEnrollmentConfig `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` + CustomerAgreement *AzureCustomerAgreementConfig `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` + PreProvisioned *AzurePreProvisionedSubscriptionConfig `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` } -type AzureEnterpriseEnrollment struct { +type AzureEnterpriseEnrollmentConfig struct { EnrollmentAccountId string `json:"enrollmentAccountId" tfsdk:"enrollment_account_id"` SubscriptionOfferType string `json:"subscriptionOfferType" tfsdk:"subscription_offer_type"` - UseLegacySubscriptionEnrollment *bool `json:"useLegacySubscriptionEnrollment,omitempty" tfsdk:"use_legacy_subscription_enrollment"` - SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` + UseLegacySubscriptionEnrollment bool `json:"useLegacySubscriptionEnrollment" tfsdk:"use_legacy_subscription_enrollment"` + SubscriptionCreationErrorCooldownSec int64 `json:"subscriptionCreationErrorCooldownSec" tfsdk:"subscription_creation_error_cooldown_sec"` } -type AzureCustomerAgreement struct { - SourceServicePrincipal *AzureSourceServicePrincipalConfig `json:"sourceServicePrincipal,omitempty" tfsdk:"source_service_principal"` - DestinationEntraId string `json:"destinationEntraId" tfsdk:"destination_entra_id"` - SourceEntraTenant string `json:"sourceEntraTenant" tfsdk:"source_entra_tenant"` - BillingScope string `json:"billingScope" tfsdk:"billing_scope"` - SubscriptionCreationErrorCooldownSec *int `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` +type AzureCustomerAgreementConfig struct { + SourceServicePrincipal AzureGraphApiCredentials `json:"sourceServicePrincipal" tfsdk:"source_service_principal"` + DestinationEntraId string `json:"destinationEntraId" tfsdk:"destination_entra_id"` + SourceEntraTenant string `json:"sourceEntraTenant" tfsdk:"source_entra_tenant"` + BillingScope string `json:"billingScope" tfsdk:"billing_scope"` + SubscriptionCreationErrorCooldownSec int64 `json:"subscriptionCreationErrorCooldownSec" tfsdk:"subscription_creation_error_cooldown_sec"` } -type AzurePreProvisioned struct { +type AzurePreProvisionedSubscriptionConfig struct { UnusedSubscriptionNamePrefix string `json:"unusedSubscriptionNamePrefix" tfsdk:"unused_subscription_name_prefix"` } -type AzureB2bUserInvitation struct { - RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` - SendAzureInvitationMail *bool `json:"sendAzureInvitationMail,omitempty" tfsdk:"send_azure_invitation_mail"` +type AzureInviteB2BUserConfig struct { + RedirectUrl string `json:"redirectUrl" tfsdk:"redirect_url"` + SendAzureInvitationMail bool `json:"sendAzureInvitationMail" tfsdk:"send_azure_invitation_mail"` } -type AzurePlatformRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AzureRole AzurePlatformRoleDefinition `json:"azureRole" tfsdk:"azure_role"` +type AzureRoleMapping struct { + MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureRole AzureRole `json:"azureRole" tfsdk:"azure_role"` } -type AzurePlatformRoleDefinition struct { +type AzureRole struct { Alias string `json:"alias" tfsdk:"alias"` Id string `json:"id" tfsdk:"id"` } @@ -84,3 +85,8 @@ type AzureTagMapper struct { Key string `json:"key" tfsdk:"key"` ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` } + +type AzureMeteringConfig struct { + ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` +} diff --git a/platform_config_azurerg.go b/platform_config_azurerg.go index 47f585a..82b5e86 100644 --- a/platform_config_azurerg.go +++ b/platform_config_azurerg.go @@ -1,19 +1,19 @@ package client type AzureRgPlatformConfig struct { - EntraTenant *string `json:"entraTenant,omitempty" tfsdk:"entra_tenant"` + EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` Replication *AzureRgReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` } type AzureRgReplicationConfig struct { - ServicePrincipal *AzureServicePrincipalConfig `json:"servicePrincipal,omitempty" tfsdk:"service_principal"` - Subscription *string `json:"subscription,omitempty" tfsdk:"subscription"` - ResourceGroupNamePattern *string `json:"resourceGroupNamePattern,omitempty" tfsdk:"resource_group_name_pattern"` - UserGroupNamePattern *string `json:"userGroupNamePattern,omitempty" tfsdk:"user_group_name_pattern"` - B2bUserInvitation *AzureB2bUserInvitation `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - UserLookUpStrategy *string `json:"userLookUpStrategy,omitempty" tfsdk:"user_look_up_strategy"` + ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + Subscription string `json:"subscription" tfsdk:"subscription"` + ResourceGroupNamePattern string `json:"resourceGroupNamePattern" tfsdk:"resource_group_name_pattern"` + UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` + B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` - AllowHierarchicalManagementGroupAssignment *bool `json:"allowHierarchicalManagementGroupAssignment,omitempty" tfsdk:"allow_hierarchical_management_group_assignment"` + AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` } diff --git a/platform_config_gcp.go b/platform_config_gcp.go index 0ee137b..e6563b9 100644 --- a/platform_config_gcp.go +++ b/platform_config_gcp.go @@ -1,22 +1,24 @@ package client type GcpPlatformConfig struct { - Replication *GcpReplicationConfig `json:"replication" tfsdk:"replication"` + Replication *GcpReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` + Metering *GcpMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type GcpReplicationConfig struct { - ServiceAccountConfig *GcpServiceAccountConfig `json:"serviceAccountConfig,omitempty" tfsdk:"service_account_config"` - Domain *string `json:"domain,omitempty" tfsdk:"domain"` - CustomerId *string `json:"customerId,omitempty" tfsdk:"customer_id"` - GroupNamePattern *string `json:"groupNamePattern,omitempty" tfsdk:"group_name_pattern"` - ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` - ProjectIdPattern *string `json:"projectIdPattern,omitempty" tfsdk:"project_id_pattern"` - BillingAccountId *string `json:"billingAccountId,omitempty" tfsdk:"billing_account_id"` - UserLookupStrategy *string `json:"userLookupStrategy,omitempty" tfsdk:"user_lookup_strategy"` - GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings,omitempty" tfsdk:"gcp_role_mappings"` - AllowHierarchicalFolderAssignment *bool `json:"allowHierarchicalFolderAssignment,omitempty" tfsdk:"allow_hierarchical_folder_assignment"` + ServiceAccountConfig GcpServiceAccountConfig `json:"serviceAccountConfig" tfsdk:"service_account_config"` + Domain string `json:"domain" tfsdk:"domain"` + CustomerId string `json:"customerId" tfsdk:"customer_id"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` + ProjectIdPattern string `json:"projectIdPattern" tfsdk:"project_id_pattern"` + BillingAccountId string `json:"billingAccountId" tfsdk:"billing_account_id"` + UserLookupStrategy string `json:"userLookupStrategy" tfsdk:"user_lookup_strategy"` + UsedExternalIdType *string `json:"usedExternalIdType,omitempty" tfsdk:"used_external_id_type"` + GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings" tfsdk:"gcp_role_mappings"` + AllowHierarchicalFolderAssignment bool `json:"allowHierarchicalFolderAssignment" tfsdk:"allow_hierarchical_folder_assignment"` TenantTags *GcpTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` } type GcpServiceAccountConfig struct { @@ -25,12 +27,12 @@ type GcpServiceAccountConfig struct { } type GcpServiceAccountCredentialsConfig struct { - ServiceAccountCredentialsB64 *string `json:"serviceAccountCredentialsB64,omitempty" tfsdk:"service_account_credentials_b64"` + ServiceAccountCredentialsB64 string `json:"serviceAccountCredentialsB64" tfsdk:"service_account_credentials_b64"` } type GcpServiceAccountWorkloadIdentityConfig struct { - Audience *string `json:"audience,omitempty" tfsdk:"audience"` - ServiceAccountEmail *string `json:"serviceAccountEmail,omitempty" tfsdk:"service_account_email"` + Audience string `json:"audience" tfsdk:"audience"` + ServiceAccountEmail string `json:"serviceAccountEmail" tfsdk:"service_account_email"` } type GcpTenantTags struct { @@ -47,3 +49,13 @@ type GcpPlatformRoleMapping struct { MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` } + +type GcpMeteringConfig struct { + ServiceAccountConfig GcpServiceAccountConfig `json:"serviceAccountConfig" tfsdk:"service_account_config"` + BigqueryTable string `json:"bigqueryTable" tfsdk:"bigquery_table"` + BigqueryTableForCarbonFootprint *string `json:"bigqueryTableForCarbonFootprint,omitempty" tfsdk:"bigquery_table_for_carbon_footprint"` + CarbonFootprintDataCollectionStartMonth *string `json:"carbonFootprintDataCollectionStartMonth,omitempty" tfsdk:"carbon_footprint_data_collection_start_month"` + PartitionTimeColumn string `json:"partitionTimeColumn" tfsdk:"partition_time_column"` + AdditionalFilter *string `json:"additionalFilter,omitempty" tfsdk:"additional_filter"` + Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` +} diff --git a/platform_config_kubernetes.go b/platform_config_kubernetes.go index 8ea5768..d817861 100644 --- a/platform_config_kubernetes.go +++ b/platform_config_kubernetes.go @@ -8,15 +8,15 @@ type KubernetesPlatformConfig struct { } type KubernetesReplicationConfig struct { - ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - NamespaceNamePattern *string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` + NamespaceNamePattern string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` } type KubernetesClientConfig struct { - AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` + AccessToken string `json:"accessToken" tfsdk:"access_token"` } type KubernetesMeteringConfig struct { - ClientConfig *KubernetesClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` + Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` } diff --git a/platform_properties_azure.go b/platform_properties_azure.go index a06c010..6979403 100644 --- a/platform_properties_azure.go +++ b/platform_properties_azure.go @@ -1,11 +1,11 @@ package client type AzurePlatformProperties struct { - AzureRoleMappings []AzureRoleMapping `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` - AzureManagementGroupId string `json:"azureManagementGroupId" tfsdk:"azure_management_group_id"` + AzureRoleMappings []AzureRoleMappingProperty `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` + AzureManagementGroupId string `json:"azureManagementGroupId" tfsdk:"azure_management_group_id"` } -type AzureRoleMapping struct { +type AzureRoleMappingProperty struct { MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` AzureGroupSuffix string `json:"azureGroupSuffix" tfsdk:"azure_group_suffix"` AzureRoleDefinitions []AzureRoleDefinition `json:"azureRoleDefinitions" tfsdk:"azure_role_definitions"` From 0b7d8f828b378605481fb0f1eb40e09858fa1ee8 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 13 Nov 2025 10:08:41 +0100 Subject: [PATCH 049/200] refactor: common data structure for tenant tags --- platform.go | 10 ++++++++++ platform_config_aws.go | 12 +----------- platform_config_azure.go | 12 +----------- platform_config_azurerg.go | 2 +- platform_config_gcp.go | 12 +----------- platform_config_openshift.go | 11 +---------- 6 files changed, 15 insertions(+), 44 deletions(-) diff --git a/platform.go b/platform.go index bc40515..01500b0 100644 --- a/platform.go +++ b/platform.go @@ -99,6 +99,16 @@ type MeshPlatformMeteringProcessingConfig struct { DeleteRawDataAfterDays int64 `json:"deleteRawDataAfterDays" tfsdk:"delete_raw_data_after_days"` } +type MeshTenantTags struct { + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers []TagMapper `json:"tagMappers" tfsdk:"tag_mappers"` +} + +type TagMapper struct { + Key string `json:"key" tfsdk:"key"` + ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` +} + func (c *MeshStackProviderClient) urlForPlatform(uuid string) *url.URL { return c.endpoints.Platforms.JoinPath(uuid) } diff --git a/platform_config_aws.go b/platform_config_aws.go index a17a4a2..6bd924c 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -15,7 +15,7 @@ type AwsReplicationConfig struct { AccountAliasPattern *string `json:"accountAliasPattern,omitempty" tfsdk:"account_alias_pattern"` EnforceAccountAlias *bool `json:"enforceAccountAlias,omitempty" tfsdk:"enforce_account_alias"` AccountEmailPattern *string `json:"accountEmailPattern,omitempty" tfsdk:"account_email_pattern"` - TenantTags *AwsTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` AwsSso *AwsSsoConfig `json:"awsSso,omitempty" tfsdk:"aws_sso"` EnrollmentConfiguration *AwsEnrollmentConfiguration `json:"enrollmentConfiguration,omitempty" tfsdk:"enrollment_configuration"` SelfDowngradeAccessRole *bool `json:"selfDowngradeAccessRole,omitempty" tfsdk:"self_downgrade_access_role"` @@ -39,16 +39,6 @@ type AwsWorkloadIdentityConfig struct { RoleArn string `json:"roleArn" tfsdk:"role_arn"` } -type AwsTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []AwsTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type AwsTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - type AwsSsoConfig struct { ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` Arn string `json:"arn" tfsdk:"arn"` diff --git a/platform_config_azure.go b/platform_config_azure.go index 33516d0..6541a42 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -15,7 +15,7 @@ type AzureReplicationConfig struct { BlueprintServicePrincipal string `json:"blueprintServicePrincipal" tfsdk:"blueprint_service_principal"` BlueprintLocation string `json:"blueprintLocation" tfsdk:"blueprint_location"` AzureRoleMappings []AzureRoleMapping `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` - TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` @@ -76,16 +76,6 @@ type AzureRole struct { Id string `json:"id" tfsdk:"id"` } -type AzureTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []AzureTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type AzureTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - type AzureMeteringConfig struct { ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` diff --git a/platform_config_azurerg.go b/platform_config_azurerg.go index 82b5e86..f3f8348 100644 --- a/platform_config_azurerg.go +++ b/platform_config_azurerg.go @@ -12,7 +12,7 @@ type AzureRgReplicationConfig struct { UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` - TenantTags *AzureTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` diff --git a/platform_config_gcp.go b/platform_config_gcp.go index e6563b9..4812173 100644 --- a/platform_config_gcp.go +++ b/platform_config_gcp.go @@ -17,7 +17,7 @@ type GcpReplicationConfig struct { UsedExternalIdType *string `json:"usedExternalIdType,omitempty" tfsdk:"used_external_id_type"` GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings" tfsdk:"gcp_role_mappings"` AllowHierarchicalFolderAssignment bool `json:"allowHierarchicalFolderAssignment" tfsdk:"allow_hierarchical_folder_assignment"` - TenantTags *GcpTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` } @@ -35,16 +35,6 @@ type GcpServiceAccountWorkloadIdentityConfig struct { ServiceAccountEmail string `json:"serviceAccountEmail" tfsdk:"service_account_email"` } -type GcpTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []GcpTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type GcpTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} - type GcpPlatformRoleMapping struct { MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` diff --git a/platform_config_openshift.go b/platform_config_openshift.go index 80f5ca6..3d2efba 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -14,7 +14,7 @@ type OpenShiftReplicationConfig struct { EnableTemplateInstantiation *bool `json:"enableTemplateInstantiation,omitempty" tfsdk:"enable_template_instantiation"` OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings,omitempty" tfsdk:"openshift_role_mappings"` IdentityProviderName *string `json:"identityProviderName,omitempty" tfsdk:"identity_provider_name"` - TenantTags *OpenShiftTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` } type OpenShiftClientConfig struct { @@ -26,15 +26,6 @@ type OpenShiftMeteringConfig struct { Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` } -type OpenShiftTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []OpenShiftTagMapper `json:"tagMappers" tfsdk:"tag_mappers"` -} - -type OpenShiftTagMapper struct { - Key string `json:"key" tfsdk:"key"` - ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` -} type OpenShiftPlatformRoleMapping struct { MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` From eb661645af97a88a1a0f339bfcf9a772a54dd1e1 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 13 Nov 2025 16:39:44 +0100 Subject: [PATCH 050/200] fix: small issues --- platform_config_aws.go | 30 +++++++++++++++--------------- platform_config_kubernetes.go | 2 +- platform_config_openshift.go | 19 +++++++------------ 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/platform_config_aws.go b/platform_config_aws.go index 6bd924c..a193b09 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -1,26 +1,26 @@ package client type AwsPlatformConfig struct { - Region *string `json:"region,omitempty" tfsdk:"region"` + Region string `json:"region,omitempty" tfsdk:"region"` Replication *AwsReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` Metering *AwsMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } type AwsReplicationConfig struct { - AccessConfig *AwsAccessConfig `json:"accessConfig,omitempty" tfsdk:"access_config"` - WaitForExternalAvm *bool `json:"waitForExternalAvm,omitempty" tfsdk:"wait_for_external_avm"` - AutomationAccountRole *string `json:"automationAccountRole,omitempty" tfsdk:"automation_account_role"` + AccessConfig AwsAccessConfig `json:"accessConfig" tfsdk:"access_config"` + WaitForExternalAvm bool `json:"waitForExternalAvm" tfsdk:"wait_for_external_avm"` + AutomationAccountRole string `json:"automationAccountRole" tfsdk:"automation_account_role"` AutomationAccountExternalId *string `json:"automationAccountExternalId,omitempty" tfsdk:"automation_account_external_id"` - AccountAccessRole *string `json:"accountAccessRole,omitempty" tfsdk:"account_access_role"` - AccountAliasPattern *string `json:"accountAliasPattern,omitempty" tfsdk:"account_alias_pattern"` - EnforceAccountAlias *bool `json:"enforceAccountAlias,omitempty" tfsdk:"enforce_account_alias"` - AccountEmailPattern *string `json:"accountEmailPattern,omitempty" tfsdk:"account_email_pattern"` + AccountAccessRole string `json:"accountAccessRole" tfsdk:"account_access_role"` + AccountAliasPattern string `json:"accountAliasPattern" tfsdk:"account_alias_pattern"` + EnforceAccountAlias bool `json:"enforceAccountAlias" tfsdk:"enforce_account_alias"` + AccountEmailPattern string `json:"accountEmailPattern" tfsdk:"account_email_pattern"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` AwsSso *AwsSsoConfig `json:"awsSso,omitempty" tfsdk:"aws_sso"` EnrollmentConfiguration *AwsEnrollmentConfiguration `json:"enrollmentConfiguration,omitempty" tfsdk:"enrollment_configuration"` - SelfDowngradeAccessRole *bool `json:"selfDowngradeAccessRole,omitempty" tfsdk:"self_downgrade_access_role"` - SkipUserGroupPermissionCleanup *bool `json:"skipUserGroupPermissionCleanup,omitempty" tfsdk:"skip_user_group_permission_cleanup"` - AllowHierarchicalOrganizationalUnitAssignment *bool `json:"allowHierarchicalOrganizationalUnitAssignment,omitempty" tfsdk:"allow_hierarchical_organizational_unit_assignment"` + SelfDowngradeAccessRole bool `json:"selfDowngradeAccessRole" tfsdk:"self_downgrade_access_role"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` + AllowHierarchicalOrganizationalUnitAssignment bool `json:"allowHierarchicalOrganizationalUnitAssignment" tfsdk:"allow_hierarchical_organizational_unit_assignment"` } type AwsAccessConfig struct { @@ -31,8 +31,8 @@ type AwsAccessConfig struct { } type AwsServiceUserConfig struct { - AccessKey string `json:"accessKey" tfsdk:"access_key"` - SecretKey *string `json:"secretKey,omitempty" tfsdk:"secret_key"` + AccessKey string `json:"accessKey" tfsdk:"access_key"` + SecretKey string `json:"secretKey" tfsdk:"secret_key"` } type AwsWorkloadIdentityConfig struct { @@ -43,9 +43,9 @@ type AwsSsoConfig struct { ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` Arn string `json:"arn" tfsdk:"arn"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - SsoAccessToken *string `json:"ssoAccessToken,omitempty" tfsdk:"sso_access_token"` + SsoAccessToken string `json:"ssoAccessToken" tfsdk:"sso_access_token"` AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` - SignInUrl *string `json:"signInUrl,omitempty" tfsdk:"sign_in_url"` + SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` } type AwsSsoRoleMapping struct { diff --git a/platform_config_kubernetes.go b/platform_config_kubernetes.go index d817861..72b0d96 100644 --- a/platform_config_kubernetes.go +++ b/platform_config_kubernetes.go @@ -9,7 +9,7 @@ type KubernetesPlatformConfig struct { type KubernetesReplicationConfig struct { ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` - NamespaceNamePattern string `json:"namespaceNamePattern,omitempty" tfsdk:"namespace_name_pattern"` + NamespaceNamePattern string `json:"namespaceNamePattern" tfsdk:"namespace_name_pattern"` } type KubernetesClientConfig struct { diff --git a/platform_config_openshift.go b/platform_config_openshift.go index 3d2efba..2dc05c7 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -8,25 +8,20 @@ type OpenShiftPlatformConfig struct { } type OpenShiftReplicationConfig struct { - ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` - ProjectNamePattern *string `json:"projectNamePattern,omitempty" tfsdk:"project_name_pattern"` - EnableTemplateInstantiation *bool `json:"enableTemplateInstantiation,omitempty" tfsdk:"enable_template_instantiation"` - OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings,omitempty" tfsdk:"openshift_role_mappings"` - IdentityProviderName *string `json:"identityProviderName,omitempty" tfsdk:"identity_provider_name"` + ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` + EnableTemplateInstantiation bool `json:"enableTemplateInstantiation" tfsdk:"enable_template_instantiation"` + OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` + IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` } -type OpenShiftClientConfig struct { - AccessToken *string `json:"accessToken,omitempty" tfsdk:"access_token"` -} - type OpenShiftMeteringConfig struct { - ClientConfig *OpenShiftClientConfig `json:"clientConfig,omitempty" tfsdk:"client_config"` - Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` + Processing MeshPlatformMeteringProcessingConfig `json:"processing" tfsdk:"processing"` } - type OpenShiftPlatformRoleMapping struct { MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` OpenShiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` From 60a7c119459b30aee27b5fa2867c648c8dc1f74a Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Mon, 17 Nov 2025 13:21:24 +0100 Subject: [PATCH 051/200] chore: generate docs --- platform_config_azurerg.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/platform_config_azurerg.go b/platform_config_azurerg.go index f3f8348..d553f56 100644 --- a/platform_config_azurerg.go +++ b/platform_config_azurerg.go @@ -6,14 +6,14 @@ type AzureRgPlatformConfig struct { } type AzureRgReplicationConfig struct { - ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` - Subscription string `json:"subscription" tfsdk:"subscription"` - ResourceGroupNamePattern string `json:"resourceGroupNamePattern" tfsdk:"resource_group_name_pattern"` - UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` - B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` - TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` - AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` + ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + Subscription string `json:"subscription" tfsdk:"subscription"` + ResourceGroupNamePattern string `json:"resourceGroupNamePattern" tfsdk:"resource_group_name_pattern"` + UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` + B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` } From 88cfb6c06e697090e29d9364414bd6f1295a7901 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Mon, 17 Nov 2025 14:43:44 +0100 Subject: [PATCH 052/200] refactor: AksServicePrincipal --- platform_config_aks.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/platform_config_aks.go b/platform_config_aks.go index 66f251f..927d119 100644 --- a/platform_config_aks.go +++ b/platform_config_aks.go @@ -8,20 +8,20 @@ type AksPlatformConfig struct { } type AksReplicationConfig struct { - AccessToken string `json:"accessToken" tfsdk:"access_token"` - NamespaceNamePattern string `json:"namespaceNamePattern" tfsdk:"namespace_name_pattern"` - GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - ServicePrincipal ServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` - AksSubscriptionId string `json:"aksSubscriptionId" tfsdk:"aks_subscription_id"` - AksClusterName string `json:"aksClusterName" tfsdk:"aks_cluster_name"` - AksResourceGroup string `json:"aksResourceGroup" tfsdk:"aks_resource_group"` - RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` - SendAzureInvitationMail bool `json:"sendAzureInvitationMail" tfsdk:"send_azure_invitation_mail"` - UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` + AccessToken string `json:"accessToken" tfsdk:"access_token"` + NamespaceNamePattern string `json:"namespaceNamePattern" tfsdk:"namespace_name_pattern"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + ServicePrincipal AksServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + AksSubscriptionId string `json:"aksSubscriptionId" tfsdk:"aks_subscription_id"` + AksClusterName string `json:"aksClusterName" tfsdk:"aks_cluster_name"` + AksResourceGroup string `json:"aksResourceGroup" tfsdk:"aks_resource_group"` + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` + SendAzureInvitationMail bool `json:"sendAzureInvitationMail" tfsdk:"send_azure_invitation_mail"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` } -type ServicePrincipalConfig struct { +type AksServicePrincipalConfig struct { ClientId string `json:"clientId" tfsdk:"client_id"` AuthType string `json:"authType" tfsdk:"auth_type"` CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` From 0b00147023e3f16bff04522da25a8540ad4a40e3 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Wed, 19 Nov 2025 12:20:19 +0100 Subject: [PATCH 053/200] feat: added payment method resource and data endpoint --- client.go | 2 + payment_method.go | 169 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 payment_method.go diff --git a/client.go b/client.go index 608b778..fb657ca 100644 --- a/client.go +++ b/client.go @@ -44,6 +44,7 @@ type endpoints struct { TagDefinitions *url.URL `json:"meshtagdefinitions"` LandingZones *url.URL `json:"meshlandingzones"` Platforms *url.URL `json:"meshplatforms"` + PaymentMethods *url.URL `json:"meshpaymentmethods"` } type loginRequest struct { @@ -80,6 +81,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), LandingZones: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlandingzones"), Platforms: rootUrl.JoinPath(apiMeshObjectsRoot, "meshplatforms"), + PaymentMethods: rootUrl.JoinPath(apiMeshObjectsRoot, "meshpaymentmethods"), } return client, nil diff --git a/payment_method.go b/payment_method.go new file mode 100644 index 0000000..2b7c249 --- /dev/null +++ b/payment_method.go @@ -0,0 +1,169 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_PAYMENT_METHOD = "application/vnd.meshcloud.api.meshpaymentmethod.v2.hal+json" + +type MeshPaymentMethod struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshPaymentMethodMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPaymentMethodSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPaymentMethodMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` +} + +type MeshPaymentMethodSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + ExpirationDate *string `json:"expirationDate,omitempty" tfsdk:"expiration_date"` + Amount *int64 `json:"amount,omitempty" tfsdk:"amount"` + Tags map[string][]string `json:"tags,omitempty" tfsdk:"tags"` +} + +type MeshPaymentMethodCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshPaymentMethodCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPaymentMethodSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPaymentMethodCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +func (c *MeshStackProviderClient) urlForPaymentMethod(workspace string, identifier string) *url.URL { + return c.endpoints.PaymentMethods.JoinPath(identifier) +} + +func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier string) (*MeshPaymentMethod, error) { + targetUrl := c.urlForPaymentMethod(workspace, identifier) + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil, nil + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var paymentMethod MeshPaymentMethod + err = json.Unmarshal(data, &paymentMethod) + if err != nil { + return nil, err + } + + return &paymentMethod, nil +} + +func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + payload, err := json.Marshal(paymentMethod) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.PaymentMethods.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) + req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdPaymentMethod MeshPaymentMethod + err = json.Unmarshal(data, &createdPaymentMethod) + if err != nil { + return nil, err + } + + return &createdPaymentMethod, nil +} + +func (c *MeshStackProviderClient) UpdatePaymentMethod(workspace string, identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + targetUrl := c.urlForPaymentMethod(workspace, identifier) + + payload, err := json.Marshal(paymentMethod) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) + req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) + + 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, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var updatedPaymentMethod MeshPaymentMethod + err = json.Unmarshal(data, &updatedPaymentMethod) + if err != nil { + return nil, err + } + + return &updatedPaymentMethod, nil +} + +func (c *MeshStackProviderClient) DeletePaymentMethod(workspace string, identifier string) error { + targetUrl := c.urlForPaymentMethod(workspace, identifier) + return c.deleteMeshObject(*targetUrl, 204) +} From a59ffa1367219a1909f508145f84909146f7aeb5 Mon Sep 17 00:00:00 2001 From: "Young-Hwan K." Date: Wed, 19 Nov 2025 12:28:58 +0100 Subject: [PATCH 054/200] Update client/payment_method.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- payment_method.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payment_method.go b/payment_method.go index 2b7c249..573f27f 100644 --- a/payment_method.go +++ b/payment_method.go @@ -44,7 +44,7 @@ type MeshPaymentMethodCreateMetadata struct { } func (c *MeshStackProviderClient) urlForPaymentMethod(workspace string, identifier string) *url.URL { - return c.endpoints.PaymentMethods.JoinPath(identifier) + return c.endpoints.PaymentMethods.JoinPath(workspace, identifier) } func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier string) (*MeshPaymentMethod, error) { From 485b1fc59d7a0b22da369afb9113ef7d25b36a87 Mon Sep 17 00:00:00 2001 From: Young-Hwan Date: Wed, 19 Nov 2025 12:30:18 +0100 Subject: [PATCH 055/200] feat: removed workspace identifier for URL creation as not required --- payment_method.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/payment_method.go b/payment_method.go index 573f27f..1555fa5 100644 --- a/payment_method.go +++ b/payment_method.go @@ -43,12 +43,12 @@ type MeshPaymentMethodCreateMetadata struct { OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -func (c *MeshStackProviderClient) urlForPaymentMethod(workspace string, identifier string) *url.URL { - return c.endpoints.PaymentMethods.JoinPath(workspace, identifier) +func (c *MeshStackProviderClient) urlForPaymentMethod(identifier string) *url.URL { + return c.endpoints.PaymentMethods.JoinPath(identifier) } func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier string) (*MeshPaymentMethod, error) { - targetUrl := c.urlForPaymentMethod(workspace, identifier) + targetUrl := c.urlForPaymentMethod(identifier) req, err := http.NewRequest("GET", targetUrl.String(), nil) if err != nil { @@ -123,8 +123,8 @@ func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPayment return &createdPaymentMethod, nil } -func (c *MeshStackProviderClient) UpdatePaymentMethod(workspace string, identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - targetUrl := c.urlForPaymentMethod(workspace, identifier) +func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + targetUrl := c.urlForPaymentMethod(identifier) payload, err := json.Marshal(paymentMethod) if err != nil { @@ -163,7 +163,7 @@ func (c *MeshStackProviderClient) UpdatePaymentMethod(workspace string, identifi return &updatedPaymentMethod, nil } -func (c *MeshStackProviderClient) DeletePaymentMethod(workspace string, identifier string) error { - targetUrl := c.urlForPaymentMethod(workspace, identifier) +func (c *MeshStackProviderClient) DeletePaymentMethod(identifier string) error { + targetUrl := c.urlForPaymentMethod(identifier) return c.deleteMeshObject(*targetUrl, 204) } From 9928220621cf2c1fa720d509e691716038b02cdd Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 19 Nov 2025 11:40:01 +0100 Subject: [PATCH 056/200] feat: quotas in landing zone data source --- landingzone.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/landingzone.go b/landingzone.go index 20cca50..c305869 100644 --- a/landingzone.go +++ b/landingzone.go @@ -25,13 +25,14 @@ type MeshLandingZoneMetadata struct { } type MeshLandingZoneSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` - AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` - InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` - PlatformRef PlatformRef `json:"platformRef" tfsdk:"platform_ref"` - PlatformProperties *PlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` + AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` + InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` + PlatformRef MeshLandingZonePlatformRef `json:"platformRef" tfsdk:"platform_ref"` + PlatformProperties *MeshLandingZonePlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` + Quotas []MeshLandingZoneQuota `json:"quotas" tfsdk:"quotas"` } type MeshLandingZoneStatus struct { @@ -39,12 +40,12 @@ type MeshLandingZoneStatus struct { Restricted bool `json:"restricted" tfsdk:"restricted"` } -type PlatformRef struct { +type MeshLandingZonePlatformRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` Kind string `json:"kind" tfsdk:"kind"` } -type PlatformProperties struct { +type MeshLandingZonePlatformProperties struct { Type string `json:"type" tfsdk:"type"` Aws *AwsPlatformProperties `json:"aws" tfsdk:"aws"` Aks *AksPlatformProperties `json:"aks" tfsdk:"aks"` @@ -55,6 +56,11 @@ type PlatformProperties struct { OpenShift *OpenShiftPlatformProperties `json:"openshift" tfsdk:"openshift"` } +type MeshLandingZoneQuota struct { + Key string `json:"key" tfsdk:"key"` + Value int64 `json:"value" tfsdk:"value"` +} + type MeshLandingZoneCreate struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` From a7a35d7704cacbc331816007267f02bd8d24b2da Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Tue, 25 Nov 2025 11:10:57 +0100 Subject: [PATCH 057/200] feat: multi select building block inputs Adds support for multi select inputs and cleans up the building block resources code. Also corrects schema restrictions for allowed input/output types. --- buildingblock.go | 1 + buildingblock_v2.go | 8 ++------ tenant_v4.go | 6 +----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index 2867be4..a63e3ee 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -14,6 +14,7 @@ const ( MESH_BUILDING_BLOCK_IO_TYPE_INTEGER = "INTEGER" MESH_BUILDING_BLOCK_IO_TYPE_BOOLEAN = "BOOLEAN" MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT = "SINGLE_SELECT" + MESH_BUILDING_BLOCK_IO_TYPE_MULTI_SELECT = "MULTI_SELECT" MESH_BUILDING_BLOCK_IO_TYPE_FILE = "FILE" MESH_BUILDING_BLOCK_IO_TYPE_LIST = "LIST" MESH_BUILDING_BLOCK_IO_TYPE_CODE = "CODE" diff --git a/buildingblock_v2.go b/buildingblock_v2.go index bf2bc55..df2b3b0 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -158,11 +158,7 @@ func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context var result *MeshBuildingBlockV2 err := retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2CompletionFunc(uuid, &result)) - if err != nil { - return nil, err - } - - return result, nil + return result, err } // waitForBuildingBlockV2CompletionFunc returns a RetryFunc that checks building block completion status @@ -176,12 +172,12 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid stri if current == nil { return retry.NonRetryableError(fmt.Errorf("building block was not found while waiting for completion")) } + *result = current // Check if we've reached a terminal state status := current.Status.Status switch status { case BUILDING_BLOCK_STATUS_SUCCEEDED: - *result = current return nil // Success, stop retrying case BUILDING_BLOCK_STATUS_FAILED: return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state", uuid)) diff --git a/tenant_v4.go b/tenant_v4.go index 4768c69..6c6a196 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -153,11 +153,7 @@ func (c *MeshStackProviderClient) PollTenantV4UntilCreation(ctx context.Context, var result *MeshTenantV4 err := retry.RetryContext(ctx, 30*time.Minute, c.waitForTenantV4CreationFunc(uuid, &result)) - if err != nil { - return nil, err - } - - return result, nil + return result, err } // waitForTenantV4CreationFunc returns a RetryFunc that checks tenant creation status From cd9785b4393e069c8ad3bb6ac7077dda1ed2b516 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 5 Dec 2025 12:34:00 +0100 Subject: [PATCH 058/200] feat: integrations client --- client.go | 4 +- integrations.go | 196 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 integrations.go diff --git a/client.go b/client.go index fb657ca..aea3f89 100644 --- a/client.go +++ b/client.go @@ -45,6 +45,7 @@ type endpoints struct { LandingZones *url.URL `json:"meshlandingzones"` Platforms *url.URL `json:"meshplatforms"` PaymentMethods *url.URL `json:"meshpaymentmethods"` + Integrations *url.URL `json:"meshintegrations"` } type loginRequest struct { @@ -82,6 +83,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro LandingZones: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlandingzones"), Platforms: rootUrl.JoinPath(apiMeshObjectsRoot, "meshplatforms"), PaymentMethods: rootUrl.JoinPath(apiMeshObjectsRoot, "meshpaymentmethods"), + Integrations: rootUrl.JoinPath(apiMeshObjectsRoot, "meshintegrations"), } return client, nil @@ -111,7 +113,7 @@ func (c *MeshStackProviderClient) login() error { if err != nil { return err } else if res.StatusCode != 200 { - return errors.New(fmt.Sprintf("Status %d: %s", res.StatusCode, ERROR_AUTHENTICATION_FAILURE)) + return fmt.Errorf("Status %d: %s", res.StatusCode, ERROR_AUTHENTICATION_FAILURE) } defer res.Body.Close() diff --git a/integrations.go b/integrations.go new file mode 100644 index 0000000..2e8545f --- /dev/null +++ b/integrations.go @@ -0,0 +1,196 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_INTEGRATION = "application/vnd.meshcloud.api.meshintegration.v1-preview.hal+json" + +type MeshIntegration struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshIntegrationMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshIntegrationSpec `json:"spec" tfsdk:"spec"` + Status *MeshIntegrationStatus `json:"status,omitempty" tfsdk:"status"` +} + +type MeshIntegrationMetadata struct { + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn *string `json:"createdOn,omitempty" tfsdk:"created_on"` +} + +type MeshIntegrationSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Config MeshIntegrationConfig `json:"config" tfsdk:"config"` +} + +type MeshIntegrationStatus struct { + IsBuiltIn bool `json:"isBuiltIn" tfsdk:"is_built_in"` + WorkloadIdentityFederation *MeshWorkloadIdentityFederation `json:"workloadIdentityFederation,omitempty" tfsdk:"workload_identity_federation"` +} + +// Integration Config wrapper with type discrimination +type MeshIntegrationConfig struct { + Type string `json:"type" tfsdk:"type"` + Github *MeshGithubIntegrationProperties `json:"github,omitempty" tfsdk:"github"` + Gitlab *MeshGitlabIntegrationProperties `json:"gitlab,omitempty" tfsdk:"gitlab"` + AzureDevops *MeshAzureDevopsIntegrationProperties `json:"azuredevops,omitempty" tfsdk:"azuredevops"` +} + +// GitHub Integration +type MeshGithubIntegrationProperties struct { + Owner string `json:"owner" tfsdk:"owner"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + AppId string `json:"appId" tfsdk:"app_id"` + AppPrivateKey string `json:"appPrivateKey" tfsdk:"app_private_key"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +// GitLab Integration +type MeshGitlabIntegrationProperties struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +// Azure DevOps Integration +type MeshAzureDevopsIntegrationProperties struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + Organization string `json:"organization" tfsdk:"organization"` + PersonalAccessToken string `json:"personalAccessToken" tfsdk:"personal_access_token"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +// Building Block Runner Reference +type BuildingBlockRunnerRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind,omitempty" tfsdk:"kind"` +} + +// Workload Identity Federation +type MeshWorkloadIdentityFederation struct { + Issuer string `json:"issuer" tfsdk:"issuer"` + Subject string `json:"subject" tfsdk:"subject"` + Gcp *MeshWifProvider `json:"gcp,omitempty" tfsdk:"gcp"` + Aws *MeshAwsWifProvider `json:"aws,omitempty" tfsdk:"aws"` + Azure *MeshWifProvider `json:"azure,omitempty" tfsdk:"azure"` +} + +type MeshWifProvider struct { + Audience string `json:"audience" tfsdk:"audience"` +} + +type MeshAwsWifProvider struct { + Audience string `json:"audience" tfsdk:"audience"` + Thumbprint string `json:"thumbprint" tfsdk:"thumbprint"` +} + +func (c *MeshStackProviderClient) urlForIntegration(workspace string, uuid string) *url.URL { + return c.endpoints.Integrations.JoinPath(workspace, uuid) +} + +func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) (*MeshIntegration, error) { + targetUrl := c.urlForIntegration(workspace, uuid) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) + + 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, err + } + + if res.StatusCode == http.StatusNotFound { + return nil, nil + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var integration MeshIntegration + err = json.Unmarshal(data, &integration) + if err != nil { + return nil, err + } + + return &integration, nil +} + +func (c *MeshStackProviderClient) ReadIntegrations(workspaceIdentifier string) (*[]MeshIntegration, error) { + var allIntegrations []MeshIntegration + + pageNumber := 0 + targetUrl := c.endpoints.Integrations + query := targetUrl.Query() + query.Set("workspaceIdentifier", workspaceIdentifier) + + 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_INTEGRATION) + + 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 !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var response struct { + Embedded struct { + MeshIntegrations []MeshIntegration `json:"meshIntegrations"` + } `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 + } + + allIntegrations = append(allIntegrations, response.Embedded.MeshIntegrations...) + + // Check if there are more pages + if response.Page.Number >= response.Page.TotalPages-1 { + break + } + + pageNumber++ + } + + return &allIntegrations, nil +} From 4225ddc63e9d508a3e49b74b38683c4bfefd257a Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 11 Dec 2025 17:24:27 +0100 Subject: [PATCH 059/200] feat: integrations data source --- integrations.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/integrations.go b/integrations.go index 2e8545f..0b686f5 100644 --- a/integrations.go +++ b/integrations.go @@ -36,14 +36,14 @@ type MeshIntegrationStatus struct { // Integration Config wrapper with type discrimination type MeshIntegrationConfig struct { - Type string `json:"type" tfsdk:"type"` - Github *MeshGithubIntegrationProperties `json:"github,omitempty" tfsdk:"github"` - Gitlab *MeshGitlabIntegrationProperties `json:"gitlab,omitempty" tfsdk:"gitlab"` - AzureDevops *MeshAzureDevopsIntegrationProperties `json:"azuredevops,omitempty" tfsdk:"azuredevops"` + Type string `json:"type" tfsdk:"type"` + Github *MeshIntegrationGithubConfig `json:"github,omitempty" tfsdk:"github"` + Gitlab *MeshIntegrationGitlabConfig `json:"gitlab,omitempty" tfsdk:"gitlab"` + AzureDevops *MeshIntegrationAzureDevopsConfig `json:"azuredevops,omitempty" tfsdk:"azuredevops"` } // GitHub Integration -type MeshGithubIntegrationProperties struct { +type MeshIntegrationGithubConfig struct { Owner string `json:"owner" tfsdk:"owner"` BaseUrl string `json:"baseUrl" tfsdk:"base_url"` AppId string `json:"appId" tfsdk:"app_id"` @@ -52,13 +52,13 @@ type MeshGithubIntegrationProperties struct { } // GitLab Integration -type MeshGitlabIntegrationProperties struct { +type MeshIntegrationGitlabConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } // Azure DevOps Integration -type MeshAzureDevopsIntegrationProperties struct { +type MeshIntegrationAzureDevopsConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` Organization string `json:"organization" tfsdk:"organization"` PersonalAccessToken string `json:"personalAccessToken" tfsdk:"personal_access_token"` @@ -68,7 +68,7 @@ type MeshAzureDevopsIntegrationProperties struct { // Building Block Runner Reference type BuildingBlockRunnerRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` - Kind string `json:"kind,omitempty" tfsdk:"kind"` + Kind string `json:"kind" tfsdk:"kind"` } // Workload Identity Federation @@ -130,13 +130,12 @@ func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) return &integration, nil } -func (c *MeshStackProviderClient) ReadIntegrations(workspaceIdentifier string) (*[]MeshIntegration, error) { +func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) { var allIntegrations []MeshIntegration pageNumber := 0 targetUrl := c.endpoints.Integrations query := targetUrl.Query() - query.Set("workspaceIdentifier", workspaceIdentifier) for { query.Set("page", fmt.Sprintf("%d", pageNumber)) From ba0a6861bda1c7d5384875ec8d39eff837217aef Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Mon, 8 Dec 2025 15:50:33 +0100 Subject: [PATCH 060/200] fix: upstream api changed secret handling --- platform.go | 5 +++++ platform_config_aks.go | 13 ++++++------- platform_config_aws.go | 23 ++++++++++++++--------- platform_config_azure.go | 19 +++++++++++-------- platform_config_azurerg.go | 2 +- platform_config_gcp.go | 13 +++++-------- platform_config_kubernetes.go | 2 +- platform_config_openshift.go | 4 ++-- 8 files changed, 45 insertions(+), 36 deletions(-) diff --git a/platform.go b/platform.go index 01500b0..a993c26 100644 --- a/platform.go +++ b/platform.go @@ -39,6 +39,11 @@ type MeshPlatformSpec struct { QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` } +type SecretEmbedded struct { + Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` + // TODO: add Hash field +} + type QuotaDefinition struct { QuotaKey string `json:"quotaKey" tfsdk:"quota_key"` MinValue int `json:"minValue" tfsdk:"min_value"` diff --git a/platform_config_aks.go b/platform_config_aks.go index 927d119..369650d 100644 --- a/platform_config_aks.go +++ b/platform_config_aks.go @@ -8,7 +8,7 @@ type AksPlatformConfig struct { } type AksReplicationConfig struct { - AccessToken string `json:"accessToken" tfsdk:"access_token"` + AccessToken SecretEmbedded `json:"accessToken" tfsdk:"access_token"` NamespaceNamePattern string `json:"namespaceNamePattern" tfsdk:"namespace_name_pattern"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` ServicePrincipal AksServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` @@ -17,16 +17,15 @@ type AksReplicationConfig struct { AksResourceGroup string `json:"aksResourceGroup" tfsdk:"aks_resource_group"` RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` SendAzureInvitationMail bool `json:"sendAzureInvitationMail" tfsdk:"send_azure_invitation_mail"` - UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + UserLookupStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` } type AksServicePrincipalConfig struct { - ClientId string `json:"clientId" tfsdk:"client_id"` - AuthType string `json:"authType" tfsdk:"auth_type"` - CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` - EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` - ObjectId string `json:"objectId" tfsdk:"object_id"` + EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` + ObjectId string `json:"objectId" tfsdk:"object_id"` + ClientId string `json:"clientId" tfsdk:"client_id"` + Auth AzureAuthConfig `json:"auth" tfsdk:"auth"` } type AksMeteringConfig struct { diff --git a/platform_config_aws.go b/platform_config_aws.go index a193b09..01805e6 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -24,18 +24,23 @@ type AwsReplicationConfig struct { } type AwsAccessConfig struct { - OrganizationRootAccountRole string `json:"organizationRootAccountRole" tfsdk:"organization_root_account_role"` - OrganizationRootAccountExternalId *string `json:"organizationRootAccountExternalId,omitempty" tfsdk:"organization_root_account_external_id"` - ServiceUserConfig *AwsServiceUserConfig `json:"serviceUserConfig,omitempty" tfsdk:"service_user_config"` - WorkloadIdentityConfig *AwsWorkloadIdentityConfig `json:"workloadIdentityConfig,omitempty" tfsdk:"workload_identity_config"` + OrganizationRootAccountRole string `json:"organizationRootAccountRole" tfsdk:"organization_root_account_role"` + OrganizationRootAccountExternalId *string `json:"organizationRootAccountExternalId,omitempty" tfsdk:"organization_root_account_external_id"` + Auth AwsAuth `json:"auth" tfsdk:"auth"` } -type AwsServiceUserConfig struct { - AccessKey string `json:"accessKey" tfsdk:"access_key"` - SecretKey string `json:"secretKey" tfsdk:"secret_key"` +type AwsAuth struct { + Type string `json:"type" tfsdk:"type"` + Credential *AwsServiceUserCredential `json:"credential,omitempty" tfsdk:"credential"` + WorkloadIdentity *AwsWorkloadIdentityCredential `json:"workloadIdentity,omitempty" tfsdk:"workload_identity"` } -type AwsWorkloadIdentityConfig struct { +type AwsServiceUserCredential struct { + AccessKey string `json:"accessKey" tfsdk:"access_key"` + SecretKey SecretEmbedded `json:"secretKey" tfsdk:"secret_key"` +} + +type AwsWorkloadIdentityCredential struct { RoleArn string `json:"roleArn" tfsdk:"role_arn"` } @@ -43,7 +48,7 @@ type AwsSsoConfig struct { ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` Arn string `json:"arn" tfsdk:"arn"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - SsoAccessToken string `json:"ssoAccessToken" tfsdk:"sso_access_token"` + SsoAccessToken SecretEmbedded `json:"ssoAccessToken" tfsdk:"sso_access_token"` AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` } diff --git a/platform_config_azure.go b/platform_config_azure.go index 6541a42..2d0375d 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -16,23 +16,26 @@ type AzureReplicationConfig struct { BlueprintLocation string `json:"blueprintLocation" tfsdk:"blueprint_location"` AzureRoleMappings []AzureRoleMapping `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` } type AzureServicePrincipalConfig struct { - ClientId string `json:"clientId" tfsdk:"client_id"` - AuthType string `json:"authType" tfsdk:"auth_type"` - CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` - ObjectId string `json:"objectId" tfsdk:"object_id"` + ClientId string `json:"clientId" tfsdk:"client_id"` + ObjectId string `json:"objectId" tfsdk:"object_id"` + Auth AzureAuthConfig `json:"auth" tfsdk:"auth"` +} + +type AzureAuthConfig struct { + Type string `json:"type" tfsdk:"type"` + Credential *SecretEmbedded `json:"credential,omitempty" tfsdk:"credential"` } type AzureGraphApiCredentials struct { - ClientId string `json:"clientId" tfsdk:"client_id"` - AuthType string `json:"authType" tfsdk:"auth_type"` - CredentialsAuthClientSecret *string `json:"credentialsAuthClientSecret,omitempty" tfsdk:"credentials_auth_client_secret"` + ClientId string `json:"clientId" tfsdk:"client_id"` + Auth AzureAuthConfig `json:"auth" tfsdk:"auth"` } type AzureSubscriptionProvisioningConfig struct { diff --git a/platform_config_azurerg.go b/platform_config_azurerg.go index d553f56..e984d30 100644 --- a/platform_config_azurerg.go +++ b/platform_config_azurerg.go @@ -11,7 +11,7 @@ type AzureRgReplicationConfig struct { ResourceGroupNamePattern string `json:"resourceGroupNamePattern" tfsdk:"resource_group_name_pattern"` UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_look_up_strategy"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` diff --git a/platform_config_gcp.go b/platform_config_gcp.go index 4812173..83cc037 100644 --- a/platform_config_gcp.go +++ b/platform_config_gcp.go @@ -6,7 +6,7 @@ type GcpPlatformConfig struct { } type GcpReplicationConfig struct { - ServiceAccountConfig GcpServiceAccountConfig `json:"serviceAccountConfig" tfsdk:"service_account_config"` + ServiceAccount GcpServiceAccountConfig `json:"serviceAccount" tfsdk:"service_account"` Domain string `json:"domain" tfsdk:"domain"` CustomerId string `json:"customerId" tfsdk:"customer_id"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` @@ -22,12 +22,9 @@ type GcpReplicationConfig struct { } type GcpServiceAccountConfig struct { - ServiceAccountCredentialsConfig *GcpServiceAccountCredentialsConfig `json:"serviceAccountCredentialsConfig,omitempty" tfsdk:"service_account_credentials_config"` - ServiceAccountWorkloadIdentityConfig *GcpServiceAccountWorkloadIdentityConfig `json:"serviceAccountWorkloadIdentityConfig,omitempty" tfsdk:"service_account_workload_identity_config"` -} - -type GcpServiceAccountCredentialsConfig struct { - ServiceAccountCredentialsB64 string `json:"serviceAccountCredentialsB64" tfsdk:"service_account_credentials_b64"` + Type string `json:"type" tfsdk:"type"` + Credential *SecretEmbedded `json:"credential,omitempty" tfsdk:"credential"` + WorkloadIdentity *GcpServiceAccountWorkloadIdentityConfig `json:"workloadIdentity,omitempty" tfsdk:"workload_identity"` } type GcpServiceAccountWorkloadIdentityConfig struct { @@ -41,7 +38,7 @@ type GcpPlatformRoleMapping struct { } type GcpMeteringConfig struct { - ServiceAccountConfig GcpServiceAccountConfig `json:"serviceAccountConfig" tfsdk:"service_account_config"` + ServiceAccount GcpServiceAccountConfig `json:"serviceAccount" tfsdk:"service_account"` BigqueryTable string `json:"bigqueryTable" tfsdk:"bigquery_table"` BigqueryTableForCarbonFootprint *string `json:"bigqueryTableForCarbonFootprint,omitempty" tfsdk:"bigquery_table_for_carbon_footprint"` CarbonFootprintDataCollectionStartMonth *string `json:"carbonFootprintDataCollectionStartMonth,omitempty" tfsdk:"carbon_footprint_data_collection_start_month"` diff --git a/platform_config_kubernetes.go b/platform_config_kubernetes.go index 72b0d96..893ba2d 100644 --- a/platform_config_kubernetes.go +++ b/platform_config_kubernetes.go @@ -13,7 +13,7 @@ type KubernetesReplicationConfig struct { } type KubernetesClientConfig struct { - AccessToken string `json:"accessToken" tfsdk:"access_token"` + AccessToken SecretEmbedded `json:"accessToken" tfsdk:"access_token"` } type KubernetesMeteringConfig struct { diff --git a/platform_config_openshift.go b/platform_config_openshift.go index 2dc05c7..1dcf722 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -12,7 +12,7 @@ type OpenShiftReplicationConfig struct { WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` EnableTemplateInstantiation bool `json:"enableTemplateInstantiation" tfsdk:"enable_template_instantiation"` - OpenShiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` + OpenshiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` } @@ -24,5 +24,5 @@ type OpenShiftMeteringConfig struct { type OpenShiftPlatformRoleMapping struct { MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - OpenShiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` + OpenshiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` } From a2c08fda0afb39d02679ef5b7aceab5daad8fb7e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 19 Dec 2025 11:41:43 +0100 Subject: [PATCH 061/200] fix: many golangci-lint issues, remove unused lookUpEndpoints in client --- buildingblock.go | 9 ++++-- buildingblock_v2.go | 18 ++++++----- client.go | 71 +++++++------------------------------------- integrations.go | 18 ++++++----- landingzone.go | 10 +++++-- payment_method.go | 12 ++++++-- platform.go | 10 +++++-- project.go | 14 ++++++--- project_binding.go | 12 +++++--- tag_definition.go | 23 ++++++++++---- tenant.go | 8 +++-- tenant_v4.go | 16 ++++++---- workspace.go | 10 +++++-- workspace_binding.go | 14 +++++---- 14 files changed, 128 insertions(+), 117 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index a63e3ee..cb3d5ab 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -94,7 +94,9 @@ func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingB return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -135,8 +137,9 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat if err != nil { return nil, err } - - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/buildingblock_v2.go b/buildingblock_v2.go index df2b3b0..095a391 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -15,7 +15,7 @@ import ( const ( CONTENT_TYPE_BUILDING_BLOCK_V2 = "application/vnd.meshcloud.api.meshbuildingblock.v2-preview.hal+json" - // Building Block Status Constants + // Building Block Status Constants. BUILDING_BLOCK_STATUS_WAITING_FOR_DEPENDENT_INPUT = "WAITING_FOR_DEPENDENT_INPUT" BUILDING_BLOCK_STATUS_WAITING_FOR_OPERATOR_INPUT = "WAITING_FOR_OPERATOR_INPUT" BUILDING_BLOCK_STATUS_PENDING = "PENDING" @@ -85,7 +85,9 @@ func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildin return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -127,7 +129,9 @@ func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2C return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -153,7 +157,7 @@ func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { } // PollBuildingBlockV2UntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) -// Returns the final building block state or an error if polling fails or times out +// Returns the final building block state or an error if polling fails or times out. func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { var result *MeshBuildingBlockV2 @@ -161,7 +165,7 @@ func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context return result, err } -// waitForBuildingBlockV2CompletionFunc returns a RetryFunc that checks building block completion status +// waitForBuildingBlockV2CompletionFunc returns a RetryFunc that checks building block completion status. func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { return func() *retry.RetryError { current, err := c.ReadBuildingBlockV2(uuid) @@ -189,12 +193,12 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid stri } // PollBuildingBlockV2UntilDeletion polls a building block until it is deleted (not found) -// Returns nil on successful deletion or an error if polling fails or times out +// Returns nil on successful deletion or an error if polling fails or times out. func (c *MeshStackProviderClient) PollBuildingBlockV2UntilDeletion(ctx context.Context, uuid string) error { return retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2DeletionFunc(uuid)) } -// waitForBuildingBlockV2DeletionFunc returns a RetryFunc that checks building block deletion status +// waitForBuildingBlockV2DeletionFunc returns a RetryFunc that checks building block deletion status. func (c *MeshStackProviderClient) waitForBuildingBlockV2DeletionFunc(uuid string) retry.RetryFunc { return func() *retry.RetryError { current, err := c.ReadBuildingBlockV2(uuid) diff --git a/client.go b/client.go index aea3f89..b7370dd 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "fmt" "io" "log" @@ -15,11 +14,6 @@ import ( const ( apiMeshObjectsRoot = "/api/meshobjects" loginEndpoint = "/api/login" - - ERROR_GENERIC_CLIENT_ERROR = "client error" - ERROR_GENERIC_API_ERROR = "api error" - ERROR_AUTHENTICATION_FAILURE = "Not authorized. Check api key and secret." - ERROR_ENDPOINT_LOOKUP = "Could not fetch endpoints for meshStack." ) type MeshStackProviderClient struct { @@ -109,14 +103,16 @@ func (c *MeshStackProviderClient) login() error { req.Header.Add("Content-Type", "application/json") res, err := c.httpClient.Do(req) - if err != nil { return err - } else if res.StatusCode != 200 { - return fmt.Errorf("Status %d: %s", res.StatusCode, ERROR_AUTHENTICATION_FAILURE) } + defer func() { + _ = res.Body.Close() + }() - defer res.Body.Close() + if res.StatusCode != 200 { + return fmt.Errorf("login failed with status %d, check api key and secret", res.StatusCode) + } data, err := io.ReadAll(res.Body) if err != nil { @@ -142,53 +138,6 @@ func (c *MeshStackProviderClient) ensureValidToken() error { return nil } -// nolint: unused -func (c *MeshStackProviderClient) lookUpEndpoints() error { - if c.ensureValidToken() != nil { - return errors.New(ERROR_AUTHENTICATION_FAILURE) - } - - meshObjectsPath, err := url.JoinPath(c.url.String(), apiMeshObjectsRoot) - if err != nil { - return err - } - meshObjects, _ := url.Parse(meshObjectsPath) - - res, err := c.httpClient.Do( - &http.Request{ - URL: meshObjects, - Method: "GET", - Header: http.Header{ - "Authorization": {c.token}, - }, - }, - ) - - if err != nil { - return errors.New(ERROR_GENERIC_CLIENT_ERROR) - } - - defer res.Body.Close() - - if res.StatusCode != 200 { - return errors.New(ERROR_AUTHENTICATION_FAILURE) - } - - data, err := io.ReadAll(res.Body) - if err != nil { - return err - } - - var endpoints endpoints - err = json.Unmarshal(data, &endpoints) - if err != nil { - return err - } - - c.endpoints = endpoints - return nil -} - func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request) (*http.Response, error) { // ensure that headeres are initialized if req.Header == nil { @@ -224,10 +173,12 @@ func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedSt res, err := c.doAuthenticatedRequest(req) if err != nil { - return errors.New(ERROR_GENERIC_CLIENT_ERROR) + return fmt.Errorf("cannot authenticate for delete request: %w ", err) } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -235,7 +186,7 @@ func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedSt } if res.StatusCode != expectedStatus { - return fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + return fmt.Errorf("expected status code %d, but got %d, body: '%s'", expectedStatus, res.StatusCode, string(data)) } return nil diff --git a/integrations.go b/integrations.go index 0b686f5..92ef1b7 100644 --- a/integrations.go +++ b/integrations.go @@ -34,7 +34,7 @@ type MeshIntegrationStatus struct { WorkloadIdentityFederation *MeshWorkloadIdentityFederation `json:"workloadIdentityFederation,omitempty" tfsdk:"workload_identity_federation"` } -// Integration Config wrapper with type discrimination +// Integration Config wrapper with type discrimination. type MeshIntegrationConfig struct { Type string `json:"type" tfsdk:"type"` Github *MeshIntegrationGithubConfig `json:"github,omitempty" tfsdk:"github"` @@ -42,7 +42,7 @@ type MeshIntegrationConfig struct { AzureDevops *MeshIntegrationAzureDevopsConfig `json:"azuredevops,omitempty" tfsdk:"azuredevops"` } -// GitHub Integration +// GitHub Integration. type MeshIntegrationGithubConfig struct { Owner string `json:"owner" tfsdk:"owner"` BaseUrl string `json:"baseUrl" tfsdk:"base_url"` @@ -51,13 +51,13 @@ type MeshIntegrationGithubConfig struct { RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } -// GitLab Integration +// GitLab Integration. type MeshIntegrationGitlabConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } -// Azure DevOps Integration +// Azure DevOps Integration. type MeshIntegrationAzureDevopsConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` Organization string `json:"organization" tfsdk:"organization"` @@ -65,13 +65,13 @@ type MeshIntegrationAzureDevopsConfig struct { RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } -// Building Block Runner Reference +// Building Block Runner Reference. type BuildingBlockRunnerRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` Kind string `json:"kind" tfsdk:"kind"` } -// Workload Identity Federation +// Workload Identity Federation. type MeshWorkloadIdentityFederation struct { Issuer string `json:"issuer" tfsdk:"issuer"` Subject string `json:"subject" tfsdk:"subject"` @@ -106,7 +106,7 @@ func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) return nil, err } - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() data, err := io.ReadAll(res.Body) if err != nil { @@ -153,7 +153,9 @@ func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/landingzone.go b/landingzone.go index c305869..fd2a371 100644 --- a/landingzone.go +++ b/landingzone.go @@ -84,7 +84,7 @@ func (c *MeshStackProviderClient) ReadLandingZone(name string) (*MeshLandingZone return nil, err } - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() if res.StatusCode == http.StatusNotFound { return nil, nil // Not found is not an error @@ -124,7 +124,9 @@ func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZone if err != nil { return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -162,7 +164,9 @@ func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *Me if err != nil { return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/payment_method.go b/payment_method.go index 1555fa5..102aef6 100644 --- a/payment_method.go +++ b/payment_method.go @@ -61,7 +61,9 @@ func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() if res.StatusCode == http.StatusNotFound { return nil, nil @@ -103,7 +105,9 @@ func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPayment return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -143,7 +147,9 @@ func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, payment return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/platform.go b/platform.go index a993c26..dd456ed 100644 --- a/platform.go +++ b/platform.go @@ -131,7 +131,7 @@ func (c *MeshStackProviderClient) ReadPlatform(uuid string) (*MeshPlatform, erro return nil, err } - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() if res.StatusCode == http.StatusNotFound { return nil, nil // Not found is not an error @@ -171,7 +171,9 @@ func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) ( if err != nil { return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -214,7 +216,9 @@ func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlat if err != nil { return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/project.go b/project.go index eca293a..eb397c4 100644 --- a/project.go +++ b/project.go @@ -60,7 +60,9 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -112,7 +114,7 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme return nil, err } - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() data, err := io.ReadAll(res.Body) if err != nil { @@ -171,7 +173,9 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -212,7 +216,9 @@ func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*Me return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/project_binding.go b/project_binding.go index 9e7138e..3d278e0 100644 --- a/project_binding.go +++ b/project_binding.go @@ -52,7 +52,7 @@ func (c *MeshStackProviderClient) readProjectBinding(name string, contentType st targetUrl = c.urlForPojectGroupBinding(name) default: - return nil, fmt.Errorf("Unexpected content type: %s", contentType) + return nil, fmt.Errorf("unexpected content type '%s'", contentType) } req, err := http.NewRequest("GET", targetUrl.String(), nil) @@ -66,7 +66,9 @@ func (c *MeshStackProviderClient) readProjectBinding(name string, contentType st return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -100,7 +102,7 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi targetUrl = c.endpoints.ProjectGroupBindings default: - return nil, fmt.Errorf("Unexpected content type: %s", contentType) + return nil, fmt.Errorf("unexpected content type '%s'", contentType) } payload, err := json.Marshal(binding) @@ -120,7 +122,9 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/tag_definition.go b/tag_definition.go index 7916b9c..0986a8e 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -113,7 +113,9 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -156,7 +158,10 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini if err != nil { return nil, err } - defer resp.Body.Close() + + defer func() { + _ = resp.Body.Close() + }() if !isSuccessHTTPStatus(resp) { return nil, fmt.Errorf("failed to read tag definition: %s", resp.Status) @@ -191,7 +196,9 @@ func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefi if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if !isSuccessHTTPStatus(resp) { return nil, fmt.Errorf("failed to create tag definition: %s", resp.Status) @@ -224,7 +231,10 @@ func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefi if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } - defer resp.Body.Close() + + defer func() { + _ = resp.Body.Close() + }() if !isSuccessHTTPStatus(resp) { return nil, fmt.Errorf("failed to update tag definition: %s", resp.Status) @@ -251,7 +261,10 @@ func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { if err != nil { return fmt.Errorf("failed to do authenticated request: %w", err) } - defer resp.Body.Close() + + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusNoContent { return fmt.Errorf("failed to delete tag definition: %s", resp.Status) diff --git a/tenant.go b/tenant.go index d10556d..3442414 100644 --- a/tenant.go +++ b/tenant.go @@ -72,7 +72,9 @@ func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, p return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -114,7 +116,9 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/tenant_v4.go b/tenant_v4.go index 6c6a196..61199fc 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -80,7 +80,9 @@ func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, erro return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -122,7 +124,9 @@ func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*M return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -148,7 +152,7 @@ func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { } // PollTenantV4UntilCreation polls a tenant until creation completes (platformTenantId is set) -// Returns the final tenant state or an error if polling fails or times out +// Returns the final tenant state or an error if polling fails or times out. func (c *MeshStackProviderClient) PollTenantV4UntilCreation(ctx context.Context, uuid string) (*MeshTenantV4, error) { var result *MeshTenantV4 @@ -156,7 +160,7 @@ func (c *MeshStackProviderClient) PollTenantV4UntilCreation(ctx context.Context, return result, err } -// waitForTenantV4CreationFunc returns a RetryFunc that checks tenant creation status +// waitForTenantV4CreationFunc returns a RetryFunc that checks tenant creation status. func (c *MeshStackProviderClient) waitForTenantV4CreationFunc(uuid string, result **MeshTenantV4) retry.RetryFunc { return func() *retry.RetryError { current, err := c.ReadTenantV4(uuid) @@ -180,12 +184,12 @@ func (c *MeshStackProviderClient) waitForTenantV4CreationFunc(uuid string, resul } // PollTenantV4UntilDeletion polls a tenant until it is deleted (not found) -// Returns nil on successful deletion or an error if polling fails or times out +// Returns nil on successful deletion or an error if polling fails or times out. func (c *MeshStackProviderClient) PollTenantV4UntilDeletion(ctx context.Context, uuid string) error { return retry.RetryContext(ctx, 30*time.Minute, c.waitForTenantV4DeletionFunc(uuid)) } -// waitForTenantV4DeletionFunc returns a RetryFunc that checks tenant deletion status +// waitForTenantV4DeletionFunc returns a RetryFunc that checks tenant deletion status. func (c *MeshStackProviderClient) waitForTenantV4DeletionFunc(uuid string) retry.RetryFunc { return func() *retry.RetryError { current, err := c.ReadTenantV4(uuid) diff --git a/workspace.go b/workspace.go index bdf878f..59adc47 100644 --- a/workspace.go +++ b/workspace.go @@ -57,7 +57,7 @@ func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, er return nil, err } - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() if res.StatusCode == http.StatusNotFound { return nil, nil // Not found is not an error @@ -97,7 +97,9 @@ func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate if err != nil { return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -135,7 +137,9 @@ func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWo if err != nil { return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { diff --git a/workspace_binding.go b/workspace_binding.go index a03b586..907ac63 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -44,7 +44,7 @@ func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType targetUrl = c.urlForWorkspaceGroupBinding(name) default: - return nil, fmt.Errorf("Unexpected content type: %s", contentType) + return nil, fmt.Errorf("unexpected content type '%s'", contentType) } req, err := http.NewRequest("GET", targetUrl.String(), nil) @@ -58,7 +58,9 @@ func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { @@ -87,12 +89,10 @@ func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceB switch contentType { case CONTENT_TYPE_WORKSPACE_USER_BINDING: targetUrl = c.endpoints.WorkspaceUserBindings - case CONTENT_TYPE_WORKSPACE_GROUP_BINDING: targetUrl = c.endpoints.WorkspaceGroupBindings - default: - return nil, fmt.Errorf("Unexpected content type: %s", contentType) + return nil, fmt.Errorf("unexpected content type '%s'", contentType) } payload, err := json.Marshal(binding) @@ -112,7 +112,9 @@ func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceB return nil, err } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() data, err := io.ReadAll(res.Body) if err != nil { From 497bda6e270aa7c54e6ecc4b577d489b86eb059f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 18 Dec 2025 12:27:25 +0100 Subject: [PATCH 062/200] feat: add meshstack_location resource --- client.go | 2 + location.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 location.go diff --git a/client.go b/client.go index b7370dd..f59759e 100644 --- a/client.go +++ b/client.go @@ -40,6 +40,7 @@ type endpoints struct { Platforms *url.URL `json:"meshplatforms"` PaymentMethods *url.URL `json:"meshpaymentmethods"` Integrations *url.URL `json:"meshintegrations"` + Locations *url.URL `json:"meshlocations"` } type loginRequest struct { @@ -78,6 +79,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro Platforms: rootUrl.JoinPath(apiMeshObjectsRoot, "meshplatforms"), PaymentMethods: rootUrl.JoinPath(apiMeshObjectsRoot, "meshpaymentmethods"), Integrations: rootUrl.JoinPath(apiMeshObjectsRoot, "meshintegrations"), + Locations: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlocations"), } return client, nil diff --git a/location.go b/location.go new file mode 100644 index 0000000..31ac263 --- /dev/null +++ b/location.go @@ -0,0 +1,174 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_LOCATION = "application/vnd.meshcloud.api.meshlocation.v1-preview.hal+json" + +type MeshLocation struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshLocationMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` + Status MeshLocationStatus `json:"status" tfsdk:"status"` +} + +type MeshLocationMetadata struct { + Name string `json:"name" tfsdk:"name"` + Uuid string `json:"uuid" tfsdk:"uuid"` +} + +type MeshLocationSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` +} + +type MeshLocationStatus struct { + IsPublic bool `json:"isPublic" tfsdk:"is_public"` +} + +type MeshLocationCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshLocationCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` +} + +type MeshLocationCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +func (c *MeshStackProviderClient) urlForLocation(name string) *url.URL { + return c.endpoints.Locations.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadLocation(name string) (*MeshLocation, error) { + targetUrl := c.urlForLocation(name) + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_LOCATION) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer func() { + _ = res.Body.Close() + }() + + if res.StatusCode == http.StatusNotFound { + return nil, nil + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var location MeshLocation + err = json.Unmarshal(data, &location) + if err != nil { + return nil, err + } + + return &location, nil +} + +func (c *MeshStackProviderClient) CreateLocation(location *MeshLocationCreate) (*MeshLocation, error) { + payload, err := json.Marshal(location) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Locations.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) + req.Header.Set("Accept", CONTENT_TYPE_LOCATION) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer func() { + _ = res.Body.Close() + }() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdLocation MeshLocation + err = json.Unmarshal(data, &createdLocation) + if err != nil { + return nil, err + } + + return &createdLocation, nil +} + +func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLocationCreate) (*MeshLocation, error) { + targetUrl := c.urlForLocation(name) + + payload, err := json.Marshal(location) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) + req.Header.Set("Accept", CONTENT_TYPE_LOCATION) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer func() { + _ = res.Body.Close() + }() + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if !isSuccessHTTPStatus(res) { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var updatedLocation MeshLocation + err = json.Unmarshal(data, &updatedLocation) + if err != nil { + return nil, err + } + + return &updatedLocation, nil +} + +func (c *MeshStackProviderClient) DeleteLocation(name string) error { + targetUrl := c.urlForLocation(name) + return c.deleteMeshObject(*targetUrl, 204) +} From 44caff03dd8d5cf7f1bde97f5e6a326133fc56bd Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Wed, 7 Jan 2026 14:38:53 +0100 Subject: [PATCH 063/200] feat: add metadata.owned_by_workspace for landing zones CU-86c75e6bt --- landingzone.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/landingzone.go b/landingzone.go index fd2a371..1b3ecf6 100644 --- a/landingzone.go +++ b/landingzone.go @@ -20,8 +20,9 @@ type MeshLandingZone struct { } type MeshLandingZoneMetadata struct { - Name string `json:"name" tfsdk:"name"` - Tags map[string][]string `json:"tags" tfsdk:"tags"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` } type MeshLandingZoneSpec struct { From 87903c19bc71ac579cd849dbe1ccfb32f1582cf4 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 14 Jan 2026 10:37:11 +0100 Subject: [PATCH 064/200] fix: building block refs required by landing zones --- buildingblock.go | 5 +++++ landingzone.go | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index cb3d5ab..c0067b0 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -76,6 +76,11 @@ type MeshBuildingBlockCreateMetadata struct { TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` } +type MeshBuildingBlockDefinitionRef struct { + Kind string `json:"kind" tfsdk:"kind"` + Uuid string `json:"uuid" tfsdk:"uuid"` +} + func (c *MeshStackProviderClient) urlForBuildingBlock(uuid string) *url.URL { return c.endpoints.BuildingBlocks.JoinPath(uuid) } diff --git a/landingzone.go b/landingzone.go index 1b3ecf6..29dcab1 100644 --- a/landingzone.go +++ b/landingzone.go @@ -26,14 +26,16 @@ type MeshLandingZoneMetadata struct { } type MeshLandingZoneSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` - AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` - InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` - PlatformRef MeshLandingZonePlatformRef `json:"platformRef" tfsdk:"platform_ref"` - PlatformProperties *MeshLandingZonePlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` - Quotas []MeshLandingZoneQuota `json:"quotas" tfsdk:"quotas"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` + AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` + InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` + PlatformRef MeshLandingZonePlatformRef `json:"platformRef" tfsdk:"platform_ref"` + PlatformProperties *MeshLandingZonePlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` + Quotas []MeshLandingZoneQuota `json:"quotas" tfsdk:"quotas"` + MandatoryBuildingBlockRefs []MeshBuildingBlockDefinitionRef `json:"mandatoryBuildingBlockRefs" tfsdk:"mandatory_building_block_refs"` + RecommendedBuildingBlockRefs []MeshBuildingBlockDefinitionRef `json:"recommendedBuildingBlockRefs" tfsdk:"recommended_building_block_refs"` } type MeshLandingZoneStatus struct { From 135bf96984d23553e123a660043a975de1a3f681 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 11:01:07 +0100 Subject: [PATCH 065/200] refactor: read and return body already in doAuthenticatedRequest, verify response with options (default verifies success) --- buildingblock.go | 41 ++++--------------- buildingblock_v2.go | 41 ++++--------------- client.go | 92 +++++++++++++++++++++++++++++------------- integrations.go | 39 ++++-------------- landingzone.go | 55 +++++-------------------- location.go | 59 +++++---------------------- payment_method.go | 59 +++++---------------------- platform.go | 55 +++++-------------------- project.go | 73 +++++---------------------------- project_binding.go | 41 ++++--------------- status_code_checker.go | 13 ------ tag_definition.go | 63 +++++------------------------ tenant.go | 42 ++++--------------- tenant_v4.go | 41 ++++--------------- workspace.go | 55 +++++-------------------- workspace_binding.go | 41 ++++--------------- 16 files changed, 179 insertions(+), 631 deletions(-) delete mode 100644 status_code_checker.go diff --git a/buildingblock.go b/buildingblock.go index c0067b0..3e2f9bc 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -94,30 +93,16 @@ func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingB } req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == 404 { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var bb MeshBuildingBlock - err = json.Unmarshal(data, &bb) + err = json.Unmarshal(body, &bb) if err != nil { return nil, err } @@ -138,25 +123,13 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK) req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } var createdBb MeshBuildingBlock - err = json.Unmarshal(data, &createdBb) + err = json.Unmarshal(body, &createdBb) if err != nil { return nil, err } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 095a391..23bce72 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -4,8 +4,8 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" - "io" "net/http" "time" @@ -80,30 +80,16 @@ func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildin } req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == 404 { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var bb MeshBuildingBlockV2 - err = json.Unmarshal(data, &bb) + err = json.Unmarshal(body, &bb) if err != nil { return nil, err } @@ -124,26 +110,13 @@ func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2C req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK_V2) req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdBb MeshBuildingBlockV2 - err = json.Unmarshal(data, &createdBb) + err = json.Unmarshal(body, &createdBb) if err != nil { return nil, err } diff --git a/client.go b/client.go index f59759e..d477b6b 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package client import ( "bytes" "encoding/json" + "errors" "fmt" "io" "log" @@ -16,6 +17,10 @@ const ( loginEndpoint = "/api/login" ) +var ( + errNotFound = errors.New("request failed with status Not Found (404)") +) + type MeshStackProviderClient struct { url *url.URL httpClient *http.Client @@ -140,8 +145,51 @@ func (c *MeshStackProviderClient) ensureValidToken() error { return nil } -func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request) (*http.Response, error) { - // ensure that headeres are initialized +type doRequestOption func(opts *doRequestOptions) + +type responseVerifier func(res *http.Response, body []byte) error + +type doRequestOptions struct { + responseVerifier responseVerifier +} + +func ensureSuccessfulRequest(res *http.Response, body []byte) error { + if res.StatusCode >= 200 && res.StatusCode <= 299 { + return nil + } + return handleErrWithNotFound(fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), res.StatusCode, body) +} + +func withExpectedStatusCode(statusCode int) doRequestOption { + return func(opts *doRequestOptions) { + opts.responseVerifier = func(res *http.Response, body []byte) error { + if res.StatusCode == statusCode { + return nil + } + return handleErrWithNotFound(fmt.Errorf("expected status %d, but got %d", statusCode, res.StatusCode), res.StatusCode, body) + } + } +} + +func handleErrWithNotFound(err error, statusCode int, body []byte) error { + errs := []error{err, fmt.Errorf("error body: %s", string(body))} + if statusCode == http.StatusNotFound { + errs = append([]error{errNotFound}, errs...) + } + return errors.Join(errs...) +} + +func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request, options ...doRequestOption) ([]byte, error) { + opts := doRequestOptions{ + // by default, verify successful response + // can be made more specific with withExpectedStatusCode option + responseVerifier: ensureSuccessfulRequest, + } + for _, option := range options { + option(&opts) + } + + // ensure that headers are initialized if req.Header == nil { req.Header = map[string][]string{} } @@ -151,8 +199,7 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request) (*ht log.Println(req) // add authentication - err := c.ensureValidToken() - if err != nil { + if err := c.ensureValidToken(); err != nil { return nil, err } req.Header.Set("Authorization", c.token) @@ -161,35 +208,26 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request) (*ht if err != nil { return nil, err } - log.Println(res) - - return res, nil -} - -func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedStatus int) error { - req, err := http.NewRequest("DELETE", targetUrl.String(), nil) - if err != nil { - return err - } - - res, err := c.doAuthenticatedRequest(req) - - if err != nil { - return fmt.Errorf("cannot authenticate for delete request: %w ", err) - } - defer func() { _ = res.Body.Close() }() + log.Println(res) - data, err := io.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { - return err + return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) } + log.Printf("Got response body with %d bytes", len(body)) + // always return body, even if the response is not successfully verified + // this allows clients to investigate the body even further if desirable. + return body, opts.responseVerifier(res, body) +} - if res.StatusCode != expectedStatus { - return fmt.Errorf("expected status code %d, but got %d, body: '%s'", expectedStatus, res.StatusCode, string(data)) +func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedStatus int) (err error) { + req, err := http.NewRequest("DELETE", targetUrl.String(), nil) + if err != nil { + return err } - - return nil + _, err = c.doAuthenticatedRequest(req, withExpectedStatusCode(expectedStatus)) + return } diff --git a/integrations.go b/integrations.go index 92ef1b7..32c192a 100644 --- a/integrations.go +++ b/integrations.go @@ -2,8 +2,8 @@ package client import ( "encoding/json" + "errors" "fmt" - "io" "net/http" "net/url" ) @@ -101,28 +101,16 @@ func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) } req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { _ = res.Body.Close() }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == http.StatusNotFound { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var integration MeshIntegration - err = json.Unmarshal(data, &integration) + err = json.Unmarshal(body, &integration) if err != nil { return nil, err } @@ -148,24 +136,11 @@ func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var response struct { Embedded struct { MeshIntegrations []MeshIntegration `json:"meshIntegrations"` @@ -178,7 +153,7 @@ func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) } `json:"page"` } - err = json.Unmarshal(data, &response) + err = json.Unmarshal(body, &response) if err != nil { return nil, err } diff --git a/landingzone.go b/landingzone.go index 29dcab1..ce88e5d 100644 --- a/landingzone.go +++ b/landingzone.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -82,28 +81,16 @@ func (c *MeshStackProviderClient) ReadLandingZone(name string) (*MeshLandingZone } req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { _ = res.Body.Close() }() - - if res.StatusCode == http.StatusNotFound { - return nil, nil // Not found is not an error + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var landingZone MeshLandingZone - err = json.Unmarshal(data, &landingZone) + err = json.Unmarshal(body, &landingZone) if err != nil { return nil, err } @@ -123,25 +110,13 @@ func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZone req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdLandingZone MeshLandingZone - err = json.Unmarshal(data, &createdLandingZone) + err = json.Unmarshal(body, &createdLandingZone) if err != nil { return nil, err } @@ -163,25 +138,13 @@ func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *Me req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var updatedLandingZone MeshLandingZone - err = json.Unmarshal(data, &updatedLandingZone) + err = json.Unmarshal(body, &updatedLandingZone) if err != nil { return nil, err } diff --git a/location.go b/location.go index 31ac263..3949f14 100644 --- a/location.go +++ b/location.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -55,30 +54,16 @@ func (c *MeshStackProviderClient) ReadLocation(name string) (*MeshLocation, erro } req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { - _ = res.Body.Close() - }() - - if res.StatusCode == http.StatusNotFound { - return nil, nil + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var location MeshLocation - err = json.Unmarshal(data, &location) + err = json.Unmarshal(body, &location) if err != nil { return nil, err } @@ -99,26 +84,13 @@ func (c *MeshStackProviderClient) CreateLocation(location *MeshLocationCreate) ( req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdLocation MeshLocation - err = json.Unmarshal(data, &createdLocation) + err = json.Unmarshal(body, &createdLocation) if err != nil { return nil, err } @@ -141,26 +113,13 @@ func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLoca req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var updatedLocation MeshLocation - err = json.Unmarshal(data, &updatedLocation) + err = json.Unmarshal(body, &updatedLocation) if err != nil { return nil, err } diff --git a/payment_method.go b/payment_method.go index 102aef6..fee94b6 100644 --- a/payment_method.go +++ b/payment_method.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -56,30 +55,16 @@ func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier } req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { - _ = res.Body.Close() - }() - - if res.StatusCode == http.StatusNotFound { - return nil, nil + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var paymentMethod MeshPaymentMethod - err = json.Unmarshal(data, &paymentMethod) + err = json.Unmarshal(body, &paymentMethod) if err != nil { return nil, err } @@ -100,26 +85,13 @@ func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPayment req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdPaymentMethod MeshPaymentMethod - err = json.Unmarshal(data, &createdPaymentMethod) + err = json.Unmarshal(body, &createdPaymentMethod) if err != nil { return nil, err } @@ -142,26 +114,13 @@ func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, payment req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var updatedPaymentMethod MeshPaymentMethod - err = json.Unmarshal(data, &updatedPaymentMethod) + err = json.Unmarshal(body, &updatedPaymentMethod) if err != nil { return nil, err } diff --git a/platform.go b/platform.go index dd456ed..3f10d26 100644 --- a/platform.go +++ b/platform.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -126,28 +125,16 @@ func (c *MeshStackProviderClient) ReadPlatform(uuid string) (*MeshPlatform, erro } req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { _ = res.Body.Close() }() - - if res.StatusCode == http.StatusNotFound { - return nil, nil // Not found is not an error + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var platform MeshPlatform - err = json.Unmarshal(data, &platform) + err = json.Unmarshal(body, &platform) if err != nil { return nil, err } @@ -167,25 +154,13 @@ func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) ( req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdPlatform MeshPlatform - err = json.Unmarshal(data, &createdPlatform) + err = json.Unmarshal(body, &createdPlatform) if err != nil { return nil, err } @@ -212,25 +187,13 @@ func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlat req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var updatedPlatform MeshPlatform - err = json.Unmarshal(data, &updatedPlatform) + err = json.Unmarshal(body, &updatedPlatform) if err != nil { return nil, err } diff --git a/project.go b/project.go index eb397c4..5abd7dd 100644 --- a/project.go +++ b/project.go @@ -3,8 +3,8 @@ package client import ( "bytes" "encoding/json" + "errors" "fmt" - "io" "net/http" "net/url" ) @@ -55,30 +55,16 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M } req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == http.StatusNotFound { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var project MeshProject - err = json.Unmarshal(data, &project) + err = json.Unmarshal(body, &project) if err != nil { return nil, err } @@ -109,22 +95,11 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { _ = res.Body.Close() }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var response struct { Embedded struct { MeshProjects []MeshProject `json:"meshProjects"` @@ -137,7 +112,7 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme } `json:"page"` } - err = json.Unmarshal(data, &response) + err = json.Unmarshal(body, &response) if err != nil { return nil, err } @@ -168,26 +143,13 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdProject MeshProject - err = json.Unmarshal(data, &createdProject) + err = json.Unmarshal(body, &createdProject) if err != nil { return nil, err } @@ -210,27 +172,14 @@ func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*Me req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var updatedProject MeshProject - err = json.Unmarshal(data, &updatedProject) + err = json.Unmarshal(body, &updatedProject) if err != nil { return nil, err } diff --git a/project_binding.go b/project_binding.go index 3d278e0..84a153b 100644 --- a/project_binding.go +++ b/project_binding.go @@ -3,8 +3,8 @@ package client import ( "bytes" "encoding/json" + "errors" "fmt" - "io" "net/http" "net/url" ) @@ -61,30 +61,16 @@ func (c *MeshStackProviderClient) readProjectBinding(name string, contentType st } req.Header.Set("Accept", contentType) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == 404 { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var binding MeshProjectBinding - err = json.Unmarshal(data, &binding) + err = json.Unmarshal(body, &binding) if err != nil { return nil, err } @@ -117,26 +103,13 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", contentType) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdBinding MeshProjectBinding - err = json.Unmarshal(data, &createdBinding) + err = json.Unmarshal(body, &createdBinding) if err != nil { return nil, err } diff --git a/status_code_checker.go b/status_code_checker.go deleted file mode 100644 index bd6f24b..0000000 --- a/status_code_checker.go +++ /dev/null @@ -1,13 +0,0 @@ -package client - -import ( - "net/http" -) - -func isSuccessHTTPStatus(resp *http.Response) bool { - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return false - } - - return true -} diff --git a/tag_definition.go b/tag_definition.go index 0986a8e..f6a95e5 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "io" "net/http" "net/url" ) @@ -108,26 +107,13 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var response tagsResponse - err = json.Unmarshal(data, &response) + err = json.Unmarshal(body, &response) if err != nil { return nil, err } @@ -154,21 +140,13 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - resp, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = resp.Body.Close() - }() - - if !isSuccessHTTPStatus(resp) { - 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 { + if err := json.Unmarshal(body, &tagDefinition); err != nil { return nil, err } @@ -192,20 +170,13 @@ func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefi req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - resp, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } - defer func() { - _ = resp.Body.Close() - }() - - if !isSuccessHTTPStatus(resp) { - return nil, fmt.Errorf("failed to create tag definition: %s", resp.Status) - } var createdTagDefinition MeshTagDefinition - if err := json.NewDecoder(resp.Body).Decode(&createdTagDefinition); err != nil { + if err := json.Unmarshal(body, &createdTagDefinition); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -227,21 +198,13 @@ func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefi req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - resp, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } - defer func() { - _ = resp.Body.Close() - }() - - if !isSuccessHTTPStatus(resp) { - return nil, fmt.Errorf("failed to update tag definition: %s", resp.Status) - } - var updatedTagDefinition MeshTagDefinition - if err := json.NewDecoder(resp.Body).Decode(&updatedTagDefinition); err != nil { + if err := json.Unmarshal(body, &updatedTagDefinition); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -257,18 +220,10 @@ func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - resp, err := c.doAuthenticatedRequest(req) + _, err = c.doAuthenticatedRequest(req, withExpectedStatusCode(http.StatusNoContent)) if err != nil { return fmt.Errorf("failed to do authenticated request: %w", err) } - defer func() { - _ = resp.Body.Close() - }() - - if resp.StatusCode != http.StatusNoContent { - return fmt.Errorf("failed to delete tag definition: %s", resp.Status) - } - return nil } diff --git a/tenant.go b/tenant.go index 3442414..05c6e11 100644 --- a/tenant.go +++ b/tenant.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -67,30 +66,16 @@ func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, p } req.Header.Set("Accept", CONTENT_TYPE_TENANT) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == 404 { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var tenant MeshTenant - err = json.Unmarshal(data, &tenant) + err = json.Unmarshal(body, &tenant) if err != nil { return nil, err } @@ -111,26 +96,13 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT req.Header.Set("Content-Type", CONTENT_TYPE_TENANT) req.Header.Set("Accept", CONTENT_TYPE_TENANT) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdTenant MeshTenant - err = json.Unmarshal(data, &createdTenant) + err = json.Unmarshal(body, &createdTenant) if err != nil { return nil, err } diff --git a/tenant_v4.go b/tenant_v4.go index 61199fc..bd3535b 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -4,8 +4,8 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" - "io" "net/http" "net/url" "time" @@ -75,30 +75,16 @@ func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, erro } req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == 404 { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var tenant MeshTenantV4 - err = json.Unmarshal(data, &tenant) + err = json.Unmarshal(body, &tenant) if err != nil { return nil, err } @@ -119,26 +105,13 @@ func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*M req.Header.Set("Content-Type", CONTENT_TYPE_TENANT_V4) req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdTenant MeshTenantV4 - err = json.Unmarshal(data, &createdTenant) + err = json.Unmarshal(body, &createdTenant) if err != nil { return nil, err } diff --git a/workspace.go b/workspace.go index 59adc47..8a3b107 100644 --- a/workspace.go +++ b/workspace.go @@ -3,8 +3,7 @@ package client import ( "bytes" "encoding/json" - "fmt" - "io" + "errors" "net/http" "net/url" ) @@ -52,28 +51,16 @@ func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, er } req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - defer func() { _ = res.Body.Close() }() - - if res.StatusCode == http.StatusNotFound { - return nil, nil // Not found is not an error + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var workspace MeshWorkspace - err = json.Unmarshal(data, &workspace) + err = json.Unmarshal(body, &workspace) if err != nil { return nil, err } @@ -93,25 +80,13 @@ func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdWorkspace MeshWorkspace - err = json.Unmarshal(data, &createdWorkspace) + err = json.Unmarshal(body, &createdWorkspace) if err != nil { return nil, err } @@ -133,25 +108,13 @@ func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWo req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var updatedWorkspace MeshWorkspace - err = json.Unmarshal(data, &updatedWorkspace) + err = json.Unmarshal(body, &updatedWorkspace) if err != nil { return nil, err } diff --git a/workspace_binding.go b/workspace_binding.go index 907ac63..5ad8033 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -3,8 +3,8 @@ package client import ( "bytes" "encoding/json" + "errors" "fmt" - "io" "net/http" "net/url" ) @@ -53,30 +53,16 @@ func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType } req.Header.Set("Accept", contentType) - res, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err + body, err := c.doAuthenticatedRequest(req) + if errors.Is(err, errNotFound) { + return nil, nil // Not found } - - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if res.StatusCode == 404 { - return nil, nil - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var binding MeshWorkspaceBinding - err = json.Unmarshal(data, &binding) + err = json.Unmarshal(body, &binding) if err != nil { return nil, err } @@ -107,26 +93,13 @@ func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceB req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", contentType) - res, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest(req) if err != nil { return nil, err } - defer func() { - _ = res.Body.Close() - }() - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - - if !isSuccessHTTPStatus(res) { - return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) - } - var createdBinding MeshWorkspaceBinding - err = json.Unmarshal(data, &createdBinding) + err = json.Unmarshal(body, &createdBinding) if err != nil { return nil, err } From 5eebb125c3a52d72d0f1eeb72a4f28621dd471b3 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 12:31:35 +0100 Subject: [PATCH 066/200] refactor: use generate unmarshalBody(IfPresent) --- buildingblock.go | 30 ++------------------ buildingblock_v2.go | 30 ++------------------ client.go | 42 ++++++++++++++++++++++++++++ integrations.go | 38 ++------------------------ landingzone.go | 40 ++------------------------- location.go | 43 ++--------------------------- payment_method.go | 43 ++--------------------------- platform.go | 40 ++------------------------- project.go | 65 ++++---------------------------------------- project_binding.go | 30 ++------------------ tag_definition.go | 54 +++++------------------------------- tenant.go | 30 ++------------------ tenant_v4.go | 30 ++------------------ workspace.go | 40 ++------------------------- workspace_binding.go | 30 ++------------------ 15 files changed, 84 insertions(+), 501 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index 3e2f9bc..c95cc0b 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -93,21 +92,7 @@ func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingB } req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var bb MeshBuildingBlock - err = json.Unmarshal(body, &bb) - if err != nil { - return nil, err - } - - return &bb, nil + return unmarshalBodyIfPresent[MeshBuildingBlock](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { @@ -123,18 +108,7 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK) req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdBb MeshBuildingBlock - err = json.Unmarshal(body, &createdBb) - if err != nil { - return nil, err - } - - return &createdBb, nil + return unmarshalBody[MeshBuildingBlock](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 23bce72..27d92b1 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "net/http" "time" @@ -80,21 +79,7 @@ func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildin } req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var bb MeshBuildingBlockV2 - err = json.Unmarshal(body, &bb) - if err != nil { - return nil, err - } - - return &bb, nil + return unmarshalBodyIfPresent[MeshBuildingBlockV2](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { @@ -110,18 +95,7 @@ func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2C req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK_V2) req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdBb MeshBuildingBlockV2 - err = json.Unmarshal(body, &createdBb) - if err != nil { - return nil, err - } - - return &createdBb, nil + return unmarshalBody[MeshBuildingBlockV2](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { diff --git a/client.go b/client.go index d477b6b..946b83f 100644 --- a/client.go +++ b/client.go @@ -231,3 +231,45 @@ func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedSt _, err = c.doAuthenticatedRequest(req, withExpectedStatusCode(expectedStatus)) return } + +func unmarshalBody[T any](body []byte, err error) (*T, error) { + if err != nil { + return nil, err + } + var target T + if err := json.Unmarshal(body, &target); err != nil { + return nil, err + } + return &target, nil +} + +func unmarshalBodyIfPresent[T any](body []byte, err error) (*T, error) { + if errors.Is(err, errNotFound) { + return nil, nil + } + return unmarshalBody[T](body, err) +} + +// paginatedResponse is a generic structure for HAL paginated responses +type paginatedResponse[T any] struct { + Embedded map[string][]T `json:"_embedded"` + Page struct { + Size int `json:"size"` + TotalElements int `json:"totalElements"` + TotalPages int `json:"totalPages"` + Number int `json:"number"` + } `json:"page"` +} + +// unmarshalPaginatedBody unmarshals a paginated HAL response and extracts items using the provided key +func unmarshalPaginatedBody[T any](body []byte, err error, embeddedKey string) ([]T, *paginatedResponse[T], error) { + if err != nil { + return nil, nil, err + } + var response paginatedResponse[T] + if err := json.Unmarshal(body, &response); err != nil { + return nil, nil, err + } + items := response.Embedded[embeddedKey] + return items, &response, nil +} diff --git a/integrations.go b/integrations.go index 32c192a..06a6014 100644 --- a/integrations.go +++ b/integrations.go @@ -1,8 +1,6 @@ package client import ( - "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -101,21 +99,7 @@ func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) } req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var integration MeshIntegration - err = json.Unmarshal(body, &integration) - if err != nil { - return nil, err - } - - return &integration, nil + return unmarshalBodyIfPresent[MeshIntegration](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) { @@ -137,28 +121,12 @@ func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) body, err := c.doAuthenticatedRequest(req) + items, response, err := unmarshalPaginatedBody[MeshIntegration](body, err, "meshIntegrations") if err != nil { return nil, err } - var response struct { - Embedded struct { - MeshIntegrations []MeshIntegration `json:"meshIntegrations"` - } `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(body, &response) - if err != nil { - return nil, err - } - - allIntegrations = append(allIntegrations, response.Embedded.MeshIntegrations...) + allIntegrations = append(allIntegrations, items...) // Check if there are more pages if response.Page.Number >= response.Page.TotalPages-1 { diff --git a/landingzone.go b/landingzone.go index ce88e5d..97a394b 100644 --- a/landingzone.go +++ b/landingzone.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -81,20 +80,7 @@ func (c *MeshStackProviderClient) ReadLandingZone(name string) (*MeshLandingZone } req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var landingZone MeshLandingZone - err = json.Unmarshal(body, &landingZone) - if err != nil { - return nil, err - } - return &landingZone, nil + return unmarshalBodyIfPresent[MeshLandingZone](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { @@ -110,17 +96,7 @@ func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZone req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdLandingZone MeshLandingZone - err = json.Unmarshal(body, &createdLandingZone) - if err != nil { - return nil, err - } - return &createdLandingZone, nil + return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { @@ -138,17 +114,7 @@ func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *Me req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var updatedLandingZone MeshLandingZone - err = json.Unmarshal(body, &updatedLandingZone) - if err != nil { - return nil, err - } - return &updatedLandingZone, nil + return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteLandingZone(name string) error { diff --git a/location.go b/location.go index 3949f14..4ff0d0d 100644 --- a/location.go +++ b/location.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -54,21 +53,7 @@ func (c *MeshStackProviderClient) ReadLocation(name string) (*MeshLocation, erro } req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var location MeshLocation - err = json.Unmarshal(body, &location) - if err != nil { - return nil, err - } - - return &location, nil + return unmarshalBodyIfPresent[MeshLocation](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateLocation(location *MeshLocationCreate) (*MeshLocation, error) { @@ -84,18 +69,7 @@ func (c *MeshStackProviderClient) CreateLocation(location *MeshLocationCreate) ( req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdLocation MeshLocation - err = json.Unmarshal(body, &createdLocation) - if err != nil { - return nil, err - } - - return &createdLocation, nil + return unmarshalBody[MeshLocation](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLocationCreate) (*MeshLocation, error) { @@ -113,18 +87,7 @@ func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLoca req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var updatedLocation MeshLocation - err = json.Unmarshal(body, &updatedLocation) - if err != nil { - return nil, err - } - - return &updatedLocation, nil + return unmarshalBody[MeshLocation](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteLocation(name string) error { diff --git a/payment_method.go b/payment_method.go index fee94b6..f62651d 100644 --- a/payment_method.go +++ b/payment_method.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -55,21 +54,7 @@ func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier } req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var paymentMethod MeshPaymentMethod - err = json.Unmarshal(body, &paymentMethod) - if err != nil { - return nil, err - } - - return &paymentMethod, nil + return unmarshalBodyIfPresent[MeshPaymentMethod](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { @@ -85,18 +70,7 @@ func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPayment req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdPaymentMethod MeshPaymentMethod - err = json.Unmarshal(body, &createdPaymentMethod) - if err != nil { - return nil, err - } - - return &createdPaymentMethod, nil + return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { @@ -114,18 +88,7 @@ func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, payment req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var updatedPaymentMethod MeshPaymentMethod - err = json.Unmarshal(body, &updatedPaymentMethod) - if err != nil { - return nil, err - } - - return &updatedPaymentMethod, nil + return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeletePaymentMethod(identifier string) error { diff --git a/platform.go b/platform.go index 3f10d26..609f8cf 100644 --- a/platform.go +++ b/platform.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -125,20 +124,7 @@ func (c *MeshStackProviderClient) ReadPlatform(uuid string) (*MeshPlatform, erro } req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var platform MeshPlatform - err = json.Unmarshal(body, &platform) - if err != nil { - return nil, err - } - return &platform, nil + return unmarshalBodyIfPresent[MeshPlatform](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) (*MeshPlatform, error) { @@ -154,17 +140,7 @@ func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) ( req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdPlatform MeshPlatform - err = json.Unmarshal(body, &createdPlatform) - if err != nil { - return nil, err - } - return &createdPlatform, nil + return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeletePlatform(uuid string) error { @@ -187,15 +163,5 @@ func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlat req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var updatedPlatform MeshPlatform - err = json.Unmarshal(body, &updatedPlatform) - if err != nil { - return nil, err - } - return &updatedPlatform, nil + return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest(req)) } diff --git a/project.go b/project.go index 5abd7dd..f18638c 100644 --- a/project.go +++ b/project.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -55,21 +54,7 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M } req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var project MeshProject - err = json.Unmarshal(body, &project) - if err != nil { - return nil, err - } - - return &project, nil + return unmarshalBodyIfPresent[MeshProject](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, paymentMethodIdentifier *string) (*[]MeshProject, error) { @@ -85,7 +70,6 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme for { query.Set("page", fmt.Sprintf("%d", pageNumber)) - targetUrl.RawQuery = query.Encode() req, err := http.NewRequest("GET", targetUrl.String(), nil) @@ -96,28 +80,12 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme req.Header.Set("Accept", CONTENT_TYPE_PROJECT) body, err := c.doAuthenticatedRequest(req) + items, response, err := unmarshalPaginatedBody[MeshProject](body, err, "meshProjects") if err != nil { return nil, err } - var response struct { - Embedded struct { - MeshProjects []MeshProject `json:"meshProjects"` - } `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(body, &response) - if err != nil { - return nil, err - } - - allProjects = append(allProjects, response.Embedded.MeshProjects...) + allProjects = append(allProjects, items...) // Check if there are more pages if response.Page.Number >= response.Page.TotalPages-1 { @@ -143,18 +111,7 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdProject MeshProject - err = json.Unmarshal(body, &createdProject) - if err != nil { - return nil, err - } - - return &createdProject, nil + return unmarshalBody[MeshProject](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*MeshProject, error) { @@ -172,19 +129,7 @@ func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*Me req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - body, err := c.doAuthenticatedRequest(req) - - if err != nil { - return nil, err - } - - var updatedProject MeshProject - err = json.Unmarshal(body, &updatedProject) - if err != nil { - return nil, err - } - - return &updatedProject, nil + return unmarshalBody[MeshProject](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteProject(workspace string, name string) error { diff --git a/project_binding.go b/project_binding.go index 84a153b..e4c01d9 100644 --- a/project_binding.go +++ b/project_binding.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -61,21 +60,7 @@ func (c *MeshStackProviderClient) readProjectBinding(name string, contentType st } req.Header.Set("Accept", contentType) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var binding MeshProjectBinding - err = json.Unmarshal(body, &binding) - if err != nil { - return nil, err - } - - return &binding, nil + return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBinding, contentType string) (*MeshProjectBinding, error) { @@ -103,16 +88,5 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", contentType) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdBinding MeshProjectBinding - err = json.Unmarshal(body, &createdBinding) - if err != nil { - return nil, err - } - - return &createdBinding, nil + return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest(req)) } diff --git a/tag_definition.go b/tag_definition.go index f6a95e5..386e1a5 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -83,21 +83,8 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er targetUrl := c.endpoints.TagDefinitions query := targetUrl.Query() - type tagsResponse 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"` - } - for { query.Set("page", fmt.Sprintf("%d", pageNumber)) - targetUrl.RawQuery = query.Encode() req, err := http.NewRequest("GET", targetUrl.String(), nil) @@ -108,17 +95,12 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) body, err := c.doAuthenticatedRequest(req) + items, response, err := unmarshalPaginatedBody[MeshTagDefinition](body, err, "meshTagDefinitions") if err != nil { return nil, err } - var response tagsResponse - err = json.Unmarshal(body, &response) - if err != nil { - return nil, err - } - - all = append(all, response.Embedded.MeshTagDefinitions...) + all = append(all, items...) // Check if there are more pages if response.Page.Number >= response.Page.TotalPages-1 { @@ -140,17 +122,7 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var tagDefinition MeshTagDefinition - if err := json.Unmarshal(body, &tagDefinition); err != nil { - return nil, err - } - - return &tagDefinition, nil + return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { @@ -170,17 +142,11 @@ func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefi req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - body, err := c.doAuthenticatedRequest(req) + result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest(req)) if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } - - var createdTagDefinition MeshTagDefinition - if err := json.Unmarshal(body, &createdTagDefinition); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return &createdTagDefinition, nil + return result, nil } func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { @@ -198,17 +164,11 @@ func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefi req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - body, err := c.doAuthenticatedRequest(req) + result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest(req)) if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } - - var updatedTagDefinition MeshTagDefinition - if err := json.Unmarshal(body, &updatedTagDefinition); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return &updatedTagDefinition, nil + return result, nil } func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { diff --git a/tenant.go b/tenant.go index 05c6e11..43f682f 100644 --- a/tenant.go +++ b/tenant.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -66,21 +65,7 @@ func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, p } req.Header.Set("Accept", CONTENT_TYPE_TENANT) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var tenant MeshTenant - err = json.Unmarshal(body, &tenant) - if err != nil { - return nil, err - } - - return &tenant, nil + return unmarshalBodyIfPresent[MeshTenant](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshTenant, error) { @@ -96,18 +81,7 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT req.Header.Set("Content-Type", CONTENT_TYPE_TENANT) req.Header.Set("Accept", CONTENT_TYPE_TENANT) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdTenant MeshTenant - err = json.Unmarshal(body, &createdTenant) - if err != nil { - return nil, err - } - - return &createdTenant, nil + return unmarshalBody[MeshTenant](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteTenant(workspace string, project string, platform string) error { diff --git a/tenant_v4.go b/tenant_v4.go index bd3535b..26c8900 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -75,21 +74,7 @@ func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, erro } req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var tenant MeshTenantV4 - err = json.Unmarshal(body, &tenant) - if err != nil { - return nil, err - } - - return &tenant, nil + return unmarshalBodyIfPresent[MeshTenantV4](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { @@ -105,18 +90,7 @@ func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*M req.Header.Set("Content-Type", CONTENT_TYPE_TENANT_V4) req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdTenant MeshTenantV4 - err = json.Unmarshal(body, &createdTenant) - if err != nil { - return nil, err - } - - return &createdTenant, nil + return unmarshalBody[MeshTenantV4](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { diff --git a/workspace.go b/workspace.go index 8a3b107..83d1eab 100644 --- a/workspace.go +++ b/workspace.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" ) @@ -51,20 +50,7 @@ func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, er } req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var workspace MeshWorkspace - err = json.Unmarshal(body, &workspace) - if err != nil { - return nil, err - } - return &workspace, nil + return unmarshalBodyIfPresent[MeshWorkspace](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { @@ -80,17 +66,7 @@ func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdWorkspace MeshWorkspace - err = json.Unmarshal(body, &createdWorkspace) - if err != nil { - return nil, err - } - return &createdWorkspace, nil + return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { @@ -108,17 +84,7 @@ func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWo req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var updatedWorkspace MeshWorkspace - err = json.Unmarshal(body, &updatedWorkspace) - if err != nil { - return nil, err - } - return &updatedWorkspace, nil + return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { diff --git a/workspace_binding.go b/workspace_binding.go index 5ad8033..326860d 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -3,7 +3,6 @@ package client import ( "bytes" "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -53,21 +52,7 @@ func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType } req.Header.Set("Accept", contentType) - body, err := c.doAuthenticatedRequest(req) - if errors.Is(err, errNotFound) { - return nil, nil // Not found - } - if err != nil { - return nil, err - } - - var binding MeshWorkspaceBinding - err = json.Unmarshal(body, &binding) - if err != nil { - return nil, err - } - - return &binding, nil + return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest(req)) } func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceBinding, contentType string) (*MeshWorkspaceBinding, error) { @@ -93,16 +78,5 @@ func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceB req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", contentType) - body, err := c.doAuthenticatedRequest(req) - if err != nil { - return nil, err - } - - var createdBinding MeshWorkspaceBinding - err = json.Unmarshal(body, &createdBinding) - if err != nil { - return nil, err - } - - return &createdBinding, nil + return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest(req)) } From 8128350065f1d58f54312795ed3c0b9a6c03132f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 14:50:10 +0100 Subject: [PATCH 067/200] refactor: use request modifiers and build http.Request only in doAuthenticatedRequest --- buildingblock.go | 33 +++----------- buildingblock_v2.go | 33 +++----------- client.go | 92 +++++++++++++++++++++++++++----------- integrations.go | 23 +++------- landingzone.go | 50 +++++---------------- location.go | 51 +++++---------------- payment_method.go | 51 +++++---------------- platform.go | 50 +++++---------------- project.go | 61 ++++++------------------- project_binding.go | 29 +++--------- project_group_binding.go | 2 +- project_user_binding.go | 2 +- tag_definition.go | 77 +++++++------------------------ tenant.go | 32 +++---------- tenant_v4.go | 32 +++---------- workspace.go | 50 +++++---------------- workspace_binding.go | 29 +++--------- workspace_group_binding.go | 2 +- workspace_user_binding.go | 2 +- 19 files changed, 194 insertions(+), 507 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index c95cc0b..bd13fe7 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -84,34 +81,18 @@ func (c *MeshStackProviderClient) urlForBuildingBlock(uuid string) *url.URL { } func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { - targetUrl := c.urlForBuildingBlock(uuid) - - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) - - return unmarshalBodyIfPresent[MeshBuildingBlock](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshBuildingBlock](c.doAuthenticatedRequest("GET", c.urlForBuildingBlock(uuid), + withAccept(CONTENT_TYPE_BUILDING_BLOCK), + )) } func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { - payload, err := json.Marshal(bb) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.BuildingBlocks.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK) - req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) - - return unmarshalBody[MeshBuildingBlock](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshBuildingBlock](c.doAuthenticatedRequest("POST", c.endpoints.BuildingBlocks, + withPayload(bb, CONTENT_TYPE_BUILDING_BLOCK), + )) } func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { targetUrl := c.urlForBuildingBlock(uuid) - return c.deleteMeshObject(*targetUrl, 202) + return c.deleteMeshObject(targetUrl, 202) } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 27d92b1..fa60a98 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -1,11 +1,8 @@ package client import ( - "bytes" "context" - "encoding/json" "fmt" - "net/http" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" @@ -71,36 +68,20 @@ type MeshBuildingBlockV2Status struct { } func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildingBlockV2, error) { - targetUrl := c.urlForBuildingBlock(uuid) - - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) - - return unmarshalBodyIfPresent[MeshBuildingBlockV2](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshBuildingBlockV2](c.doAuthenticatedRequest("GET", c.urlForBuildingBlock(uuid), + withAccept(CONTENT_TYPE_BUILDING_BLOCK_V2), + )) } func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { - payload, err := json.Marshal(bb) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.BuildingBlocks.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK_V2) - req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) - - return unmarshalBody[MeshBuildingBlockV2](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshBuildingBlockV2](c.doAuthenticatedRequest("POST", c.endpoints.BuildingBlocks, + withPayload(bb, CONTENT_TYPE_BUILDING_BLOCK_V2), + )) } func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { targetUrl := c.urlForBuildingBlock(uuid) - return c.deleteMeshObject(*targetUrl, 202) + return c.deleteMeshObject(targetUrl, 202) } // PollBuildingBlockV2UntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) diff --git a/client.go b/client.go index 946b83f..8ac114f 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "net/url" + "slices" "time" ) @@ -147,19 +148,16 @@ func (c *MeshStackProviderClient) ensureValidToken() error { type doRequestOption func(opts *doRequestOptions) +type requestModifier func(req *http.Request) + type responseVerifier func(res *http.Response, body []byte) error type doRequestOptions struct { + requestPayload any + requestModifiers []requestModifier responseVerifier responseVerifier } -func ensureSuccessfulRequest(res *http.Response, body []byte) error { - if res.StatusCode >= 200 && res.StatusCode <= 299 { - return nil - } - return handleErrWithNotFound(fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), res.StatusCode, body) -} - func withExpectedStatusCode(statusCode int) doRequestOption { return func(opts *doRequestOptions) { opts.responseVerifier = func(res *http.Response, body []byte) error { @@ -171,6 +169,38 @@ func withExpectedStatusCode(statusCode int) doRequestOption { } } +func withAccept(accept string) doRequestOption { + return withHeader("Accept", accept) +} + +func withHeader(key, value string) doRequestOption { + return func(opts *doRequestOptions) { + opts.requestModifiers = append(opts.requestModifiers, func(req *http.Request) { + req.Header.Set(key, value) + }) + } +} + +func withPayload(payload any, contentType string) doRequestOption { + return func(opts *doRequestOptions) { + // always provide Accept header with the same value as content-type, + // as meshObject API currently does not version that differently. + // that convention can still be overridden/broken by a later withAccept option + withAccept(contentType)(opts) + withHeader("Content-Type", contentType)(opts) + opts.requestPayload = payload + } +} + +func ensureSuccessfulRequest(opts *doRequestOptions) { + opts.responseVerifier = func(res *http.Response, body []byte) error { + if res.StatusCode >= 200 && res.StatusCode <= 299 { + return nil + } + return handleErrWithNotFound(fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), res.StatusCode, body) + } +} + func handleErrWithNotFound(err error, statusCode int, body []byte) error { errs := []error{err, fmt.Errorf("error body: %s", string(body))} if statusCode == http.StatusNotFound { @@ -179,22 +209,34 @@ func handleErrWithNotFound(err error, statusCode int, body []byte) error { return errors.Join(errs...) } -func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request, options ...doRequestOption) ([]byte, error) { - opts := doRequestOptions{ +func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { + // prepend (aka insert at 0) some default options such that given options may be overridden by caller + options = slices.Insert(options, 0, + withHeader("User-Agent", "meshStack Terraform Provider"), // by default, verify successful response // can be made more specific with withExpectedStatusCode option - responseVerifier: ensureSuccessfulRequest, - } + ensureSuccessfulRequest, + ) + opts := doRequestOptions{} for _, option := range options { option(&opts) } - // ensure that headers are initialized - if req.Header == nil { - req.Header = map[string][]string{} + var requestBody io.ReadWriter + if opts.requestPayload != nil { + requestBody = new(bytes.Buffer) + if err := json.NewEncoder(requestBody).Encode(opts.requestPayload); err != nil { + return nil, fmt.Errorf("failed to encode request body payload: %w", err) + } } - req.Header.Set("User-Agent", "meshStack Terraform Provider") + req, err := http.NewRequest(method, url.String(), requestBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + for _, requestModifier := range opts.requestModifiers { + requestModifier(req) + } // log request before adding auth log.Println(req) @@ -213,22 +255,18 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(req *http.Request, opti }() log.Println(res) - body, err := io.ReadAll(res.Body) + responseBody, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) } - log.Printf("Got response body with %d bytes", len(body)) - // always return body, even if the response is not successfully verified - // this allows clients to investigate the body even further if desirable. - return body, opts.responseVerifier(res, body) + log.Printf("Got response body with %d bytes", len(responseBody)) + // always return responseBody, even if the response is not successfully verified + // this allows clients to investigate the responseBody even further if desirable. + return responseBody, opts.responseVerifier(res, responseBody) } -func (c *MeshStackProviderClient) deleteMeshObject(targetUrl url.URL, expectedStatus int) (err error) { - req, err := http.NewRequest("DELETE", targetUrl.String(), nil) - if err != nil { - return err - } - _, err = c.doAuthenticatedRequest(req, withExpectedStatusCode(expectedStatus)) +func (c *MeshStackProviderClient) deleteMeshObject(targetUrl *url.URL, expectedStatus int) (err error) { + _, err = c.doAuthenticatedRequest("DELETE", targetUrl, withExpectedStatusCode(expectedStatus)) return } @@ -261,7 +299,7 @@ type paginatedResponse[T any] struct { } `json:"page"` } -// unmarshalPaginatedBody unmarshals a paginated HAL response and extracts items using the provided key +// unmarshalPaginatedBody unmarshalls a paginated HAL response and extracts items using the provided key func unmarshalPaginatedBody[T any](body []byte, err error, embeddedKey string) ([]T, *paginatedResponse[T], error) { if err != nil { return nil, nil, err diff --git a/integrations.go b/integrations.go index 06a6014..a495046 100644 --- a/integrations.go +++ b/integrations.go @@ -2,7 +2,6 @@ package client import ( "fmt" - "net/http" "net/url" ) @@ -92,14 +91,9 @@ func (c *MeshStackProviderClient) urlForIntegration(workspace string, uuid strin } func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) (*MeshIntegration, error) { - targetUrl := c.urlForIntegration(workspace, uuid) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_INTEGRATION) - - return unmarshalBodyIfPresent[MeshIntegration](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshIntegration](c.doAuthenticatedRequest("GET", c.urlForIntegration(workspace, uuid), + withAccept(CONTENT_TYPE_INTEGRATION), + )) } func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) { @@ -113,14 +107,9 @@ func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) 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_INTEGRATION) - - body, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest("GET", targetUrl, + withAccept(CONTENT_TYPE_INTEGRATION), + ) items, response, err := unmarshalPaginatedBody[MeshIntegration](body, err, "meshIntegrations") if err != nil { return nil, err diff --git a/landingzone.go b/landingzone.go index 97a394b..a84d1c8 100644 --- a/landingzone.go +++ b/landingzone.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -73,51 +70,24 @@ func (c *MeshStackProviderClient) urlForLandingZone(name string) *url.URL { } func (c *MeshStackProviderClient) ReadLandingZone(name string) (*MeshLandingZone, error) { - targetUrl := c.urlForLandingZone(name) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - - return unmarshalBodyIfPresent[MeshLandingZone](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshLandingZone](c.doAuthenticatedRequest("GET", c.urlForLandingZone(name), + withAccept(CONTENT_TYPE_LANDINGZONE), + )) } func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - payload, err := json.Marshal(landingZone) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.LandingZones.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) - req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - - return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest("POST", c.endpoints.LandingZones, + withPayload(landingZone, CONTENT_TYPE_LANDINGZONE), + )) } func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - targetUrl := c.urlForLandingZone(name) - - payload, err := json.Marshal(landingZone) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_LANDINGZONE) - req.Header.Set("Accept", CONTENT_TYPE_LANDINGZONE) - - return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest("PUT", c.urlForLandingZone(name), + withPayload(landingZone, CONTENT_TYPE_LANDINGZONE), + )) } func (c *MeshStackProviderClient) DeleteLandingZone(name string) error { targetUrl := c.urlForLandingZone(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/location.go b/location.go index 4ff0d0d..c82fc02 100644 --- a/location.go +++ b/location.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -45,52 +42,24 @@ func (c *MeshStackProviderClient) urlForLocation(name string) *url.URL { } func (c *MeshStackProviderClient) ReadLocation(name string) (*MeshLocation, error) { - targetUrl := c.urlForLocation(name) - - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - - return unmarshalBodyIfPresent[MeshLocation](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshLocation](c.doAuthenticatedRequest("GET", c.urlForLocation(name), + withAccept(CONTENT_TYPE_LOCATION), + )) } func (c *MeshStackProviderClient) CreateLocation(location *MeshLocationCreate) (*MeshLocation, error) { - payload, err := json.Marshal(location) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.Locations.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) - req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - - return unmarshalBody[MeshLocation](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshLocation](c.doAuthenticatedRequest("POST", c.endpoints.Locations, + withPayload(location, CONTENT_TYPE_LOCATION), + )) } func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLocationCreate) (*MeshLocation, error) { - targetUrl := c.urlForLocation(name) - - payload, err := json.Marshal(location) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_LOCATION) - req.Header.Set("Accept", CONTENT_TYPE_LOCATION) - - return unmarshalBody[MeshLocation](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshLocation](c.doAuthenticatedRequest("PUT", c.urlForLocation(name), + withPayload(location, CONTENT_TYPE_LOCATION), + )) } func (c *MeshStackProviderClient) DeleteLocation(name string) error { targetUrl := c.urlForLocation(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/payment_method.go b/payment_method.go index f62651d..7145079 100644 --- a/payment_method.go +++ b/payment_method.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -46,52 +43,24 @@ func (c *MeshStackProviderClient) urlForPaymentMethod(identifier string) *url.UR } func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier string) (*MeshPaymentMethod, error) { - targetUrl := c.urlForPaymentMethod(identifier) - - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - - return unmarshalBodyIfPresent[MeshPaymentMethod](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshPaymentMethod](c.doAuthenticatedRequest("GET", c.urlForPaymentMethod(identifier), + withAccept(CONTENT_TYPE_PAYMENT_METHOD), + )) } func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - payload, err := json.Marshal(paymentMethod) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.PaymentMethods.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) - req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - - return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest("POST", c.endpoints.PaymentMethods, + withPayload(paymentMethod, CONTENT_TYPE_PAYMENT_METHOD), + )) } func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - targetUrl := c.urlForPaymentMethod(identifier) - - payload, err := json.Marshal(paymentMethod) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PAYMENT_METHOD) - req.Header.Set("Accept", CONTENT_TYPE_PAYMENT_METHOD) - - return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest("PUT", c.urlForPaymentMethod(identifier), + withPayload(paymentMethod, CONTENT_TYPE_PAYMENT_METHOD), + )) } func (c *MeshStackProviderClient) DeletePaymentMethod(identifier string) error { targetUrl := c.urlForPaymentMethod(identifier) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/platform.go b/platform.go index 609f8cf..58e3219 100644 --- a/platform.go +++ b/platform.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -117,51 +114,24 @@ func (c *MeshStackProviderClient) urlForPlatform(uuid string) *url.URL { } func (c *MeshStackProviderClient) ReadPlatform(uuid string) (*MeshPlatform, error) { - targetUrl := c.urlForPlatform(uuid) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - - return unmarshalBodyIfPresent[MeshPlatform](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshPlatform](c.doAuthenticatedRequest("GET", c.urlForPlatform(uuid), + withAccept(CONTENT_TYPE_PLATFORM), + )) } func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) (*MeshPlatform, error) { - payload, err := json.Marshal(platform) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.Platforms.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) - req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - - return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest("POST", c.endpoints.Platforms, + withPayload(platform, CONTENT_TYPE_PLATFORM), + )) } func (c *MeshStackProviderClient) DeletePlatform(uuid string) error { targetUrl := c.urlForPlatform(uuid) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { - targetUrl := c.urlForPlatform(uuid) - - payload, err := json.Marshal(platform) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PLATFORM) - req.Header.Set("Accept", CONTENT_TYPE_PLATFORM) - - return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest("PUT", c.urlForPlatform(uuid), + withPayload(platform, CONTENT_TYPE_PLATFORM), + )) } diff --git a/project.go b/project.go index f18638c..0ddec95 100644 --- a/project.go +++ b/project.go @@ -1,10 +1,7 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "net/http" "net/url" ) @@ -47,14 +44,9 @@ func (c *MeshStackProviderClient) urlForProject(workspace string, name string) * } func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*MeshProject, error) { - targetUrl := c.urlForProject(workspace, name) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - - return unmarshalBodyIfPresent[MeshProject](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshProject](c.doAuthenticatedRequest("GET", c.urlForProject(workspace, name), + withAccept(CONTENT_TYPE_PROJECT), + )) } func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, paymentMethodIdentifier *string) (*[]MeshProject, error) { @@ -72,14 +64,9 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme 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_PROJECT) - - body, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest("GET", targetUrl, + withAccept(CONTENT_TYPE_PROJECT), + ) items, response, err := unmarshalPaginatedBody[MeshProject](body, err, "meshProjects") if err != nil { return nil, err @@ -99,40 +86,18 @@ func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, payme } func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*MeshProject, error) { - payload, err := json.Marshal(project) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.Projects.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) - req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - - return unmarshalBody[MeshProject](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshProject](c.doAuthenticatedRequest("POST", c.endpoints.Projects, + withPayload(project, CONTENT_TYPE_PROJECT), + )) } func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*MeshProject, error) { - targetUrl := c.urlForProject(project.Metadata.OwnedByWorkspace, project.Metadata.Name) - - payload, err := json.Marshal(project) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_PROJECT) - req.Header.Set("Accept", CONTENT_TYPE_PROJECT) - - return unmarshalBody[MeshProject](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshProject](c.doAuthenticatedRequest("PUT", c.urlForProject(project.Metadata.OwnedByWorkspace, project.Metadata.Name), + withPayload(project, CONTENT_TYPE_PROJECT), + )) } func (c *MeshStackProviderClient) DeleteProject(workspace string, name string) error { targetUrl := c.urlForProject(workspace, name) - return c.deleteMeshObject(*targetUrl, 202) + return c.deleteMeshObject(targetUrl, 202) } diff --git a/project_binding.go b/project_binding.go index e4c01d9..4c924eb 100644 --- a/project_binding.go +++ b/project_binding.go @@ -1,10 +1,7 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "net/http" "net/url" ) @@ -54,13 +51,9 @@ func (c *MeshStackProviderClient) readProjectBinding(name string, contentType st return nil, fmt.Errorf("unexpected content type '%s'", contentType) } - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", contentType) - - return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest("GET", targetUrl, + withAccept(contentType), + )) } func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBinding, contentType string) (*MeshProjectBinding, error) { @@ -76,17 +69,7 @@ func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBindi return nil, fmt.Errorf("unexpected content type '%s'", contentType) } - payload, err := json.Marshal(binding) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", contentType) - req.Header.Set("Accept", contentType) - - return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest("POST", targetUrl, + withPayload(binding, contentType), + )) } diff --git a/project_group_binding.go b/project_group_binding.go index 8b66818..7044724 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -22,5 +22,5 @@ func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProject func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { targetUrl := c.urlForPojectGroupBinding(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/project_user_binding.go b/project_user_binding.go index 3de8e22..bfa689d 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -22,5 +22,5 @@ func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectU func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { targetUrl := c.urlForPojectUserBinding(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/tag_definition.go b/tag_definition.go index 386e1a5..469faca 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -1,10 +1,7 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "net/http" "net/url" ) @@ -87,14 +84,9 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er 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) - - body, err := c.doAuthenticatedRequest(req) + body, err := c.doAuthenticatedRequest("GET", targetUrl, + withAccept(CONTENT_TYPE_TAG_DEFINITION), + ) items, response, err := unmarshalPaginatedBody[MeshTagDefinition](body, err, "meshTagDefinitions") if err != nil { return nil, err @@ -114,35 +106,15 @@ func (c *MeshStackProviderClient) ReadTagDefinitions() (*[]MeshTagDefinition, er } 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 - } - - req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - - return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("GET", c.urlForTagDefinition(name), + withAccept(CONTENT_TYPE_TAG_DEFINITION), + )) } func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - targetUrl := c.endpoints.TagDefinitions - data, err := json.Marshal(tagDefinition) - if err != nil { - return nil, fmt.Errorf("failed to marshal tag definition: %w", err) - } - - fmt.Printf("JSON Payload: %s\n", string(data)) - - req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) - req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - - result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest(req)) + result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("POST", c.endpoints.TagDefinitions, + withPayload(tagDefinition, CONTENT_TYPE_TAG_DEFINITION), + )) if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } @@ -150,21 +122,9 @@ func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefi } func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - targetUrl := c.urlForTagDefinition(tagDefinition.Metadata.Name) - data, err := json.Marshal(tagDefinition) - if err != nil { - return nil, fmt.Errorf("failed to marshal tag definition: %w", err) - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", CONTENT_TYPE_TAG_DEFINITION) - req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - - result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest(req)) + result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("PUT", c.urlForTagDefinition(tagDefinition.Metadata.Name), + withPayload(tagDefinition, CONTENT_TYPE_TAG_DEFINITION), + )) if err != nil { return nil, fmt.Errorf("failed to do authenticated request: %w", err) } @@ -172,15 +132,10 @@ func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefi } func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { - targetUrl := c.urlForTagDefinition(name) - req, err := http.NewRequest("DELETE", targetUrl.String(), nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Accept", CONTENT_TYPE_TAG_DEFINITION) - - _, err = c.doAuthenticatedRequest(req, withExpectedStatusCode(http.StatusNoContent)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForTagDefinition(name), + withAccept(CONTENT_TYPE_TAG_DEFINITION), + withExpectedStatusCode(204), + ) if err != nil { return fmt.Errorf("failed to do authenticated request: %w", err) } diff --git a/tenant.go b/tenant.go index 43f682f..b43c902 100644 --- a/tenant.go +++ b/tenant.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -58,33 +55,18 @@ func (c *MeshStackProviderClient) urlForTenant(workspace string, project string, } func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, platform string) (*MeshTenant, error) { - targetUrl := c.urlForTenant(workspace, project, platform) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_TENANT) - - return unmarshalBodyIfPresent[MeshTenant](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshTenant](c.doAuthenticatedRequest("GET", c.urlForTenant(workspace, project, platform), + withAccept(CONTENT_TYPE_TENANT), + )) } func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshTenant, error) { - payload, err := json.Marshal(tenant) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.Tenants.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_TENANT) - req.Header.Set("Accept", CONTENT_TYPE_TENANT) - - return unmarshalBody[MeshTenant](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshTenant](c.doAuthenticatedRequest("POST", c.endpoints.Tenants, + withPayload(tenant, CONTENT_TYPE_TENANT), + )) } func (c *MeshStackProviderClient) DeleteTenant(workspace string, project string, platform string) error { targetUrl := c.urlForTenant(workspace, project, platform) - return c.deleteMeshObject(*targetUrl, 202) + return c.deleteMeshObject(targetUrl, 202) } diff --git a/tenant_v4.go b/tenant_v4.go index 26c8900..d9dd909 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -1,11 +1,8 @@ package client import ( - "bytes" "context" - "encoding/json" "fmt" - "net/http" "net/url" "time" @@ -67,35 +64,20 @@ func (c *MeshStackProviderClient) urlForTenantV4(uuid string) *url.URL { } func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, error) { - targetUrl := c.urlForTenantV4(uuid) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) - - return unmarshalBodyIfPresent[MeshTenantV4](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshTenantV4](c.doAuthenticatedRequest("GET", c.urlForTenantV4(uuid), + withAccept(CONTENT_TYPE_TENANT_V4), + )) } func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { - payload, err := json.Marshal(tenant) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.Tenants.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_TENANT_V4) - req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4) - - return unmarshalBody[MeshTenantV4](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshTenantV4](c.doAuthenticatedRequest("POST", c.endpoints.Tenants, + withPayload(tenant, CONTENT_TYPE_TENANT_V4), + )) } func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { targetUrl := c.urlForTenantV4(uuid) - return c.deleteMeshObject(*targetUrl, 202) + return c.deleteMeshObject(targetUrl, 202) } // PollTenantV4UntilCreation polls a tenant until creation completes (platformTenantId is set) diff --git a/workspace.go b/workspace.go index 83d1eab..10e72a2 100644 --- a/workspace.go +++ b/workspace.go @@ -1,9 +1,6 @@ package client import ( - "bytes" - "encoding/json" - "net/http" "net/url" ) @@ -43,51 +40,24 @@ func (c *MeshStackProviderClient) urlForWorkspace(name string) *url.URL { } func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, error) { - targetUrl := c.urlForWorkspace(name) - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - - return unmarshalBodyIfPresent[MeshWorkspace](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshWorkspace](c.doAuthenticatedRequest("GET", c.urlForWorkspace(name), + withAccept(CONTENT_TYPE_WORKSPACE), + )) } func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - payload, err := json.Marshal(workspace) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", c.endpoints.Workspaces.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) - req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - - return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest("POST", c.endpoints.Workspaces, + withPayload(workspace, CONTENT_TYPE_WORKSPACE), + )) } func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - targetUrl := c.urlForWorkspace(name) - - payload, err := json.Marshal(workspace) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) - req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) - - return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest("PUT", c.urlForWorkspace(name), + withPayload(workspace, CONTENT_TYPE_WORKSPACE), + )) } func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { targetUrl := c.urlForWorkspace(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/workspace_binding.go b/workspace_binding.go index 326860d..17355fd 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -1,10 +1,7 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "net/http" "net/url" ) @@ -46,13 +43,9 @@ func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType return nil, fmt.Errorf("unexpected content type '%s'", contentType) } - req, err := http.NewRequest("GET", targetUrl.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", contentType) - - return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest(req)) + return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest("GET", targetUrl, + withAccept(contentType), + )) } func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceBinding, contentType string) (*MeshWorkspaceBinding, error) { @@ -66,17 +59,7 @@ func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceB return nil, fmt.Errorf("unexpected content type '%s'", contentType) } - payload, err := json.Marshal(binding) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", targetUrl.String(), bytes.NewBuffer(payload)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", contentType) - req.Header.Set("Accept", contentType) - - return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest(req)) + return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest("POST", targetUrl, + withPayload(binding, contentType), + )) } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index a32a582..1844cd9 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -22,5 +22,5 @@ func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorks func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { targetUrl := c.urlForWorkspaceGroupBinding(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index 3019df8..28f6a21 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -22,5 +22,5 @@ func (c *MeshStackProviderClient) CreateWorkspaceUserBinding(binding *MeshWorksp func (c *MeshStackProviderClient) DeleteWorkspaceUserBinding(name string) error { targetUrl := c.urlForWorkspaceUserBinding(name) - return c.deleteMeshObject(*targetUrl, 204) + return c.deleteMeshObject(targetUrl, 204) } From 63a8c50e5ff25e140557100383d22e97692a2a31 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 15:21:07 +0100 Subject: [PATCH 068/200] refactor: remove deleteMeshObject and overeager status code validation, remove responseVerifier --- buildingblock.go | 4 +- buildingblock_v2.go | 4 +- client.go | 141 ++++++++++++------------------------- landingzone.go | 4 +- location.go | 4 +- payment_method.go | 4 +- platform.go | 4 +- project.go | 4 +- project_group_binding.go | 4 +- project_user_binding.go | 4 +- tag_definition.go | 19 +---- tenant.go | 4 +- tenant_v4.go | 4 +- workspace.go | 4 +- workspace_group_binding.go | 4 +- workspace_user_binding.go | 4 +- 16 files changed, 75 insertions(+), 141 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index bd13fe7..430cfa1 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -93,6 +93,6 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat } func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { - targetUrl := c.urlForBuildingBlock(uuid) - return c.deleteMeshObject(targetUrl, 202) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid)) + return err } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index fa60a98..77d72d9 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -80,8 +80,8 @@ func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2C } func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { - targetUrl := c.urlForBuildingBlock(uuid) - return c.deleteMeshObject(targetUrl, 202) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid)) + return err } // PollBuildingBlockV2UntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) diff --git a/client.go b/client.go index 8ac114f..771f0ab 100644 --- a/client.go +++ b/client.go @@ -13,11 +13,6 @@ import ( "time" ) -const ( - apiMeshObjectsRoot = "/api/meshobjects" - loginEndpoint = "/api/login" -) - var ( errNotFound = errors.New("request failed with status Not Found (404)") ) @@ -49,16 +44,6 @@ type endpoints struct { Locations *url.URL `json:"meshlocations"` } -type loginRequest struct { - ClientId string `json:"clientId"` - ClientSecret string `json:"clientSecret"` -} - -type loginResponse struct { - Token string `json:"access_token"` - ExpireSec int `json:"expires_in"` -} - func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackProviderClient, error) { client := &MeshStackProviderClient{ url: rootUrl, @@ -71,6 +56,9 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro } // TODO: lookup endpoints + const ( + apiMeshObjectsRoot = "/api/meshobjects" + ) client.endpoints = endpoints{ BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), @@ -92,50 +80,27 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro } func (c *MeshStackProviderClient) login() error { - loginPath, err := url.JoinPath(c.url.String(), loginEndpoint) - if err != nil { - return err - } + loginUrl := c.url.JoinPath("/api/login") - loginRequest := loginRequest{ - ClientId: c.apiKey, - ClientSecret: c.apiSecret, + type loginRequest struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` } - payload, err := json.Marshal(loginRequest) - if err != nil { - return err + type loginResponse struct { + Token string `json:"access_token"` + ExpireSec int `json:"expires_in"` } - req, _ := http.NewRequest(http.MethodPost, loginPath, bytes.NewBuffer(payload)) - req.Header.Add("Content-Type", "application/json") - - res, err := c.httpClient.Do(req) - if err != nil { - return err - } - defer func() { - _ = res.Body.Close() - }() - - if res.StatusCode != 200 { - return fmt.Errorf("login failed with status %d, check api key and secret", res.StatusCode) - } - - data, err := io.ReadAll(res.Body) - if err != nil { - return err - } - - var loginResult loginResponse - err = json.Unmarshal(data, &loginResult) + loginResult, err := unmarshalBody[loginResponse](c.doRequest("POST", loginUrl, + withPayload(loginRequest{ClientId: c.apiKey, ClientSecret: c.apiSecret}, "application/json")), + ) if err != nil { - return err + return fmt.Errorf("login request to %s with API Key '%s' failed: %w", loginUrl, c.apiKey, err) } c.token = fmt.Sprintf("Bearer %s", loginResult.Token) c.tokenExpiry = time.Now().Add(time.Second * time.Duration(loginResult.ExpireSec)) - return nil } @@ -150,22 +115,14 @@ type doRequestOption func(opts *doRequestOptions) type requestModifier func(req *http.Request) -type responseVerifier func(res *http.Response, body []byte) error - type doRequestOptions struct { requestPayload any requestModifiers []requestModifier - responseVerifier responseVerifier } -func withExpectedStatusCode(statusCode int) doRequestOption { +func appendRequestModifier(modifier requestModifier) doRequestOption { return func(opts *doRequestOptions) { - opts.responseVerifier = func(res *http.Response, body []byte) error { - if res.StatusCode == statusCode { - return nil - } - return handleErrWithNotFound(fmt.Errorf("expected status %d, but got %d", statusCode, res.StatusCode), res.StatusCode, body) - } + opts.requestModifiers = append(opts.requestModifiers, modifier) } } @@ -174,11 +131,9 @@ func withAccept(accept string) doRequestOption { } func withHeader(key, value string) doRequestOption { - return func(opts *doRequestOptions) { - opts.requestModifiers = append(opts.requestModifiers, func(req *http.Request) { - req.Header.Set(key, value) - }) - } + return appendRequestModifier(func(req *http.Request) { + req.Header.Set(key, value) + }) } func withPayload(payload any, contentType string) doRequestOption { @@ -192,30 +147,10 @@ func withPayload(payload any, contentType string) doRequestOption { } } -func ensureSuccessfulRequest(opts *doRequestOptions) { - opts.responseVerifier = func(res *http.Response, body []byte) error { - if res.StatusCode >= 200 && res.StatusCode <= 299 { - return nil - } - return handleErrWithNotFound(fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), res.StatusCode, body) - } -} - -func handleErrWithNotFound(err error, statusCode int, body []byte) error { - errs := []error{err, fmt.Errorf("error body: %s", string(body))} - if statusCode == http.StatusNotFound { - errs = append([]error{errNotFound}, errs...) - } - return errors.Join(errs...) -} - -func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { +func (c *MeshStackProviderClient) doRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { // prepend (aka insert at 0) some default options such that given options may be overridden by caller options = slices.Insert(options, 0, withHeader("User-Agent", "meshStack Terraform Provider"), - // by default, verify successful response - // can be made more specific with withExpectedStatusCode option - ensureSuccessfulRequest, ) opts := doRequestOptions{} for _, option := range options { @@ -237,14 +172,6 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url for _, requestModifier := range opts.requestModifiers { requestModifier(req) } - // log request before adding auth - log.Println(req) - - // add authentication - if err := c.ensureValidToken(); err != nil { - return nil, err - } - req.Header.Set("Authorization", c.token) res, err := c.httpClient.Do(req) if err != nil { @@ -260,14 +187,34 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) } log.Printf("Got response body with %d bytes", len(responseBody)) + + if res.StatusCode >= 200 && res.StatusCode <= 299 { + return responseBody, nil + } + var errs []error + if res.StatusCode == http.StatusNotFound { + errs = append(errs, errNotFound) + } + errs = append(errs, + fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), + fmt.Errorf("error response: %s", string(responseBody)), + ) // always return responseBody, even if the response is not successfully verified // this allows clients to investigate the responseBody even further if desirable. - return responseBody, opts.responseVerifier(res, responseBody) + return responseBody, errors.Join(errs...) } -func (c *MeshStackProviderClient) deleteMeshObject(targetUrl *url.URL, expectedStatus int) (err error) { - _, err = c.doAuthenticatedRequest("DELETE", targetUrl, withExpectedStatusCode(expectedStatus)) - return +func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { + if err := c.ensureValidToken(); err != nil { + return nil, err + } + return c.doRequest(method, url, append(options, + appendRequestModifier(func(req *http.Request) { + // log request before adding Authorization header below + log.Println(req) + }), + withHeader("Authorization", c.token), + )...) } func unmarshalBody[T any](body []byte, err error) (*T, error) { diff --git a/landingzone.go b/landingzone.go index a84d1c8..c643199 100644 --- a/landingzone.go +++ b/landingzone.go @@ -88,6 +88,6 @@ func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *Me } func (c *MeshStackProviderClient) DeleteLandingZone(name string) error { - targetUrl := c.urlForLandingZone(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForLandingZone(name)) + return err } diff --git a/location.go b/location.go index c82fc02..aa82a1e 100644 --- a/location.go +++ b/location.go @@ -60,6 +60,6 @@ func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLoca } func (c *MeshStackProviderClient) DeleteLocation(name string) error { - targetUrl := c.urlForLocation(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForLocation(name)) + return err } diff --git a/payment_method.go b/payment_method.go index 7145079..11b2c27 100644 --- a/payment_method.go +++ b/payment_method.go @@ -61,6 +61,6 @@ func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, payment } func (c *MeshStackProviderClient) DeletePaymentMethod(identifier string) error { - targetUrl := c.urlForPaymentMethod(identifier) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPaymentMethod(identifier)) + return err } diff --git a/platform.go b/platform.go index 58e3219..c6e2a70 100644 --- a/platform.go +++ b/platform.go @@ -126,8 +126,8 @@ func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) ( } func (c *MeshStackProviderClient) DeletePlatform(uuid string) error { - targetUrl := c.urlForPlatform(uuid) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPlatform(uuid)) + return err } func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { diff --git a/project.go b/project.go index 0ddec95..c8a8dc0 100644 --- a/project.go +++ b/project.go @@ -98,6 +98,6 @@ func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*Me } func (c *MeshStackProviderClient) DeleteProject(workspace string, name string) error { - targetUrl := c.urlForProject(workspace, name) - return c.deleteMeshObject(targetUrl, 202) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForProject(workspace, name)) + return err } diff --git a/project_group_binding.go b/project_group_binding.go index 7044724..d815b17 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -21,6 +21,6 @@ func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProject } func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { - targetUrl := c.urlForPojectGroupBinding(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectGroupBinding(name)) + return err } diff --git a/project_user_binding.go b/project_user_binding.go index bfa689d..22614e2 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -21,6 +21,6 @@ func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectU } func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { - targetUrl := c.urlForPojectUserBinding(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectUserBinding(name)) + return err } diff --git a/tag_definition.go b/tag_definition.go index 469faca..ed34177 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -112,33 +112,20 @@ func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefini } func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("POST", c.endpoints.TagDefinitions, + return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("POST", c.endpoints.TagDefinitions, withPayload(tagDefinition, CONTENT_TYPE_TAG_DEFINITION), )) - if err != nil { - return nil, fmt.Errorf("failed to do authenticated request: %w", err) - } - return result, nil } func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - result, err := unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("PUT", c.urlForTagDefinition(tagDefinition.Metadata.Name), + return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("PUT", c.urlForTagDefinition(tagDefinition.Metadata.Name), withPayload(tagDefinition, CONTENT_TYPE_TAG_DEFINITION), )) - if err != nil { - return nil, fmt.Errorf("failed to do authenticated request: %w", err) - } - return result, nil } func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { _, err := c.doAuthenticatedRequest("DELETE", c.urlForTagDefinition(name), withAccept(CONTENT_TYPE_TAG_DEFINITION), - withExpectedStatusCode(204), ) - if err != nil { - return fmt.Errorf("failed to do authenticated request: %w", err) - } - - return nil + return err } diff --git a/tenant.go b/tenant.go index b43c902..7aba3bb 100644 --- a/tenant.go +++ b/tenant.go @@ -67,6 +67,6 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT } func (c *MeshStackProviderClient) DeleteTenant(workspace string, project string, platform string) error { - targetUrl := c.urlForTenant(workspace, project, platform) - return c.deleteMeshObject(targetUrl, 202) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenant(workspace, project, platform)) + return err } diff --git a/tenant_v4.go b/tenant_v4.go index d9dd909..2a771ab 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -76,8 +76,8 @@ func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*M } func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { - targetUrl := c.urlForTenantV4(uuid) - return c.deleteMeshObject(targetUrl, 202) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenantV4(uuid)) + return err } // PollTenantV4UntilCreation polls a tenant until creation completes (platformTenantId is set) diff --git a/workspace.go b/workspace.go index 10e72a2..4020d76 100644 --- a/workspace.go +++ b/workspace.go @@ -58,6 +58,6 @@ func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWo } func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { - targetUrl := c.urlForWorkspace(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspace(name)) + return err } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 1844cd9..7139c26 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -21,6 +21,6 @@ func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorks } func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { - targetUrl := c.urlForWorkspaceGroupBinding(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceGroupBinding(name)) + return err } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index 28f6a21..5ae4ce8 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -21,6 +21,6 @@ func (c *MeshStackProviderClient) CreateWorkspaceUserBinding(binding *MeshWorksp } func (c *MeshStackProviderClient) DeleteWorkspaceUserBinding(name string) error { - targetUrl := c.urlForWorkspaceUserBinding(name) - return c.deleteMeshObject(targetUrl, 204) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceUserBinding(name)) + return err } From 10f6f889e8f3cd58424e3eaecb94fd85025b94d7 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 15:25:40 +0100 Subject: [PATCH 069/200] refactor: simplify client for workspace/project binding --- project_binding.go | 41 -------------------------------------- project_group_binding.go | 8 ++++++-- project_user_binding.go | 8 ++++++-- workspace_binding.go | 39 ------------------------------------ workspace_group_binding.go | 8 ++++++-- workspace_user_binding.go | 8 ++++++-- 6 files changed, 24 insertions(+), 88 deletions(-) diff --git a/project_binding.go b/project_binding.go index 4c924eb..96e8b69 100644 --- a/project_binding.go +++ b/project_binding.go @@ -1,10 +1,5 @@ package client -import ( - "fmt" - "net/url" -) - type MeshProjectBinding struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -37,39 +32,3 @@ type MeshProjectTargetRef struct { type MeshSubject struct { Name string `json:"name" tfsdk:"name"` } - -func (c *MeshStackProviderClient) readProjectBinding(name string, contentType string) (*MeshProjectBinding, error) { - var targetUrl *url.URL - switch contentType { - case CONTENT_TYPE_PROJECT_USER_BINDING: - targetUrl = c.urlForPojectUserBinding(name) - - case CONTENT_TYPE_PROJECT_GROUP_BINDING: - targetUrl = c.urlForPojectGroupBinding(name) - - default: - return nil, fmt.Errorf("unexpected content type '%s'", contentType) - } - - return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest("GET", targetUrl, - withAccept(contentType), - )) -} - -func (c *MeshStackProviderClient) createProjectBinding(binding *MeshProjectBinding, contentType string) (*MeshProjectBinding, error) { - var targetUrl *url.URL - switch contentType { - case CONTENT_TYPE_PROJECT_USER_BINDING: - targetUrl = c.endpoints.ProjectUserBindings - - case CONTENT_TYPE_PROJECT_GROUP_BINDING: - targetUrl = c.endpoints.ProjectGroupBindings - - default: - return nil, fmt.Errorf("unexpected content type '%s'", contentType) - } - - return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest("POST", targetUrl, - withPayload(binding, contentType), - )) -} diff --git a/project_group_binding.go b/project_group_binding.go index d815b17..a947af7 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -13,11 +13,15 @@ func (c *MeshStackProviderClient) urlForPojectGroupBinding(name string) *url.URL } func (c *MeshStackProviderClient) ReadProjectGroupBinding(name string) (*MeshProjectGroupBinding, error) { - return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_GROUP_BINDING) + return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest("GET", c.urlForPojectGroupBinding(name), + withAccept(CONTENT_TYPE_PROJECT_GROUP_BINDING), + )) } func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { - return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_GROUP_BINDING) + return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest("POST", c.endpoints.ProjectGroupBindings, + withPayload(binding, CONTENT_TYPE_PROJECT_GROUP_BINDING), + )) } func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { diff --git a/project_user_binding.go b/project_user_binding.go index 22614e2..7708748 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -13,11 +13,15 @@ func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL } func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { - return c.readProjectBinding(name, CONTENT_TYPE_PROJECT_USER_BINDING) + return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest("GET", c.urlForPojectUserBinding(name), + withAccept(CONTENT_TYPE_PROJECT_USER_BINDING), + )) } func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - return c.createProjectBinding(binding, CONTENT_TYPE_PROJECT_USER_BINDING) + return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest("POST", c.endpoints.ProjectUserBindings, + withPayload(binding, CONTENT_TYPE_PROJECT_USER_BINDING), + )) } func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { diff --git a/workspace_binding.go b/workspace_binding.go index 17355fd..8732b7c 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -1,10 +1,5 @@ package client -import ( - "fmt" - "net/url" -) - type MeshWorkspaceBinding struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -29,37 +24,3 @@ type MeshWorkspaceTargetRef struct { type MeshWorkspaceSubject struct { Name string `json:"name" tfsdk:"name"` } - -func (c *MeshStackProviderClient) readWorkspaceBinding(name string, contentType string) (*MeshWorkspaceBinding, error) { - var targetUrl *url.URL - switch contentType { - case CONTENT_TYPE_WORKSPACE_USER_BINDING: - targetUrl = c.urlForWorkspaceUserBinding(name) - - case CONTENT_TYPE_WORKSPACE_GROUP_BINDING: - targetUrl = c.urlForWorkspaceGroupBinding(name) - - default: - return nil, fmt.Errorf("unexpected content type '%s'", contentType) - } - - return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest("GET", targetUrl, - withAccept(contentType), - )) -} - -func (c *MeshStackProviderClient) createWorkspaceBinding(binding *MeshWorkspaceBinding, contentType string) (*MeshWorkspaceBinding, error) { - var targetUrl *url.URL - switch contentType { - case CONTENT_TYPE_WORKSPACE_USER_BINDING: - targetUrl = c.endpoints.WorkspaceUserBindings - case CONTENT_TYPE_WORKSPACE_GROUP_BINDING: - targetUrl = c.endpoints.WorkspaceGroupBindings - default: - return nil, fmt.Errorf("unexpected content type '%s'", contentType) - } - - return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest("POST", targetUrl, - withPayload(binding, contentType), - )) -} diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 7139c26..c2df99d 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -13,11 +13,15 @@ func (c *MeshStackProviderClient) urlForWorkspaceGroupBinding(name string) *url. } func (c *MeshStackProviderClient) ReadWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { - return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) + return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest("GET", c.urlForWorkspaceGroupBinding(name), + withAccept(CONTENT_TYPE_WORKSPACE_GROUP_BINDING), + )) } func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { - return c.createWorkspaceBinding(binding, CONTENT_TYPE_WORKSPACE_GROUP_BINDING) + return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest("POST", c.endpoints.WorkspaceGroupBindings, + withPayload(binding, CONTENT_TYPE_WORKSPACE_GROUP_BINDING), + )) } func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { diff --git a/workspace_user_binding.go b/workspace_user_binding.go index 5ae4ce8..de5a4c7 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -13,11 +13,15 @@ func (c *MeshStackProviderClient) urlForWorkspaceUserBinding(name string) *url.U } func (c *MeshStackProviderClient) ReadWorkspaceUserBinding(name string) (*MeshWorkspaceUserBinding, error) { - return c.readWorkspaceBinding(name, CONTENT_TYPE_WORKSPACE_USER_BINDING) + return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest("GET", c.urlForWorkspaceUserBinding(name), + withAccept(CONTENT_TYPE_WORKSPACE_USER_BINDING), + )) } func (c *MeshStackProviderClient) CreateWorkspaceUserBinding(binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { - return c.createWorkspaceBinding(binding, CONTENT_TYPE_WORKSPACE_USER_BINDING) + return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest("POST", c.endpoints.WorkspaceUserBindings, + withPayload(binding, CONTENT_TYPE_WORKSPACE_USER_BINDING), + )) } func (c *MeshStackProviderClient) DeleteWorkspaceUserBinding(name string) error { From 41f210e76cbd0bde59eb2ea52962413e3e0ade93 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 15:28:57 +0100 Subject: [PATCH 070/200] fix: provide (versioning) Accept header when calling DELETE endpoints --- buildingblock.go | 4 +++- buildingblock_v2.go | 4 +++- landingzone.go | 4 +++- location.go | 4 +++- payment_method.go | 4 +++- platform.go | 4 +++- project.go | 4 +++- project_group_binding.go | 4 +++- project_user_binding.go | 4 +++- tenant.go | 4 +++- tenant_v4.go | 4 +++- workspace.go | 4 +++- workspace_group_binding.go | 4 +++- workspace_user_binding.go | 4 +++- 14 files changed, 42 insertions(+), 14 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index 430cfa1..f807a17 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -93,6 +93,8 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat } func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid), + withAccept(CONTENT_TYPE_BUILDING_BLOCK), + ) return err } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 77d72d9..6f86a1b 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -80,7 +80,9 @@ func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2C } func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid), + withAccept(CONTENT_TYPE_BUILDING_BLOCK_V2), + ) return err } diff --git a/landingzone.go b/landingzone.go index c643199..c5f3ca9 100644 --- a/landingzone.go +++ b/landingzone.go @@ -88,6 +88,8 @@ func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *Me } func (c *MeshStackProviderClient) DeleteLandingZone(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForLandingZone(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForLandingZone(name), + withAccept(CONTENT_TYPE_LANDINGZONE), + ) return err } diff --git a/location.go b/location.go index aa82a1e..e0a6474 100644 --- a/location.go +++ b/location.go @@ -60,6 +60,8 @@ func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLoca } func (c *MeshStackProviderClient) DeleteLocation(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForLocation(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForLocation(name), + withAccept(CONTENT_TYPE_LOCATION), + ) return err } diff --git a/payment_method.go b/payment_method.go index 11b2c27..0649836 100644 --- a/payment_method.go +++ b/payment_method.go @@ -61,6 +61,8 @@ func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, payment } func (c *MeshStackProviderClient) DeletePaymentMethod(identifier string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPaymentMethod(identifier)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPaymentMethod(identifier), + withAccept(CONTENT_TYPE_PAYMENT_METHOD), + ) return err } diff --git a/platform.go b/platform.go index c6e2a70..e4d0081 100644 --- a/platform.go +++ b/platform.go @@ -126,7 +126,9 @@ func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) ( } func (c *MeshStackProviderClient) DeletePlatform(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPlatform(uuid)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPlatform(uuid), + withAccept(CONTENT_TYPE_PLATFORM), + ) return err } diff --git a/project.go b/project.go index c8a8dc0..3138794 100644 --- a/project.go +++ b/project.go @@ -98,6 +98,8 @@ func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*Me } func (c *MeshStackProviderClient) DeleteProject(workspace string, name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForProject(workspace, name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForProject(workspace, name), + withAccept(CONTENT_TYPE_PROJECT), + ) return err } diff --git a/project_group_binding.go b/project_group_binding.go index a947af7..4737824 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -25,6 +25,8 @@ func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProject } func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectGroupBinding(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectGroupBinding(name), + withAccept(CONTENT_TYPE_PROJECT_GROUP_BINDING), + ) return err } diff --git a/project_user_binding.go b/project_user_binding.go index 7708748..9ade8d5 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -25,6 +25,8 @@ func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectU } func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectUserBinding(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectUserBinding(name), + withAccept(CONTENT_TYPE_PROJECT_USER_BINDING), + ) return err } diff --git a/tenant.go b/tenant.go index 7aba3bb..b187f83 100644 --- a/tenant.go +++ b/tenant.go @@ -67,6 +67,8 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT } func (c *MeshStackProviderClient) DeleteTenant(workspace string, project string, platform string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenant(workspace, project, platform)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenant(workspace, project, platform), + withAccept(CONTENT_TYPE_TENANT), + ) return err } diff --git a/tenant_v4.go b/tenant_v4.go index 2a771ab..6243a95 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -76,7 +76,9 @@ func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*M } func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenantV4(uuid)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenantV4(uuid), + withAccept(CONTENT_TYPE_TENANT_V4), + ) return err } diff --git a/workspace.go b/workspace.go index 4020d76..309ec38 100644 --- a/workspace.go +++ b/workspace.go @@ -58,6 +58,8 @@ func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWo } func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspace(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspace(name), + withAccept(CONTENT_TYPE_WORKSPACE), + ) return err } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index c2df99d..2fddb5c 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -25,6 +25,8 @@ func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorks } func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceGroupBinding(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceGroupBinding(name), + withAccept(CONTENT_TYPE_WORKSPACE_GROUP_BINDING), + ) return err } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index de5a4c7..c10d77c 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -25,6 +25,8 @@ func (c *MeshStackProviderClient) CreateWorkspaceUserBinding(binding *MeshWorksp } func (c *MeshStackProviderClient) DeleteWorkspaceUserBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceUserBinding(name)) + _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceUserBinding(name), + withAccept(CONTENT_TYPE_WORKSPACE_USER_BINDING), + ) return err } From 6bfabe8d2d697f5588cde39d075cd834a70c1b0f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 16:34:37 +0100 Subject: [PATCH 071/200] refactor: simplify fetching paginated responses, add simplistic data source test --- client.go | 103 ++++++++++++++++++++++++++++++++++++---------- integrations.go | 33 +-------------- project.go | 40 ++++-------------- tag_definition.go | 33 +-------------- 4 files changed, 93 insertions(+), 116 deletions(-) diff --git a/client.go b/client.go index 771f0ab..3a56804 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "iter" "log" "net/http" "net/url" @@ -113,19 +114,42 @@ func (c *MeshStackProviderClient) ensureValidToken() error { type doRequestOption func(opts *doRequestOptions) +type urlModifier func(url *url.URL) + type requestModifier func(req *http.Request) type doRequestOptions struct { + urlModifiers []urlModifier requestPayload any requestModifiers []requestModifier } +func appendUrlModifier(modifier urlModifier) doRequestOption { + return func(opts *doRequestOptions) { + opts.urlModifiers = append(opts.urlModifiers, modifier) + } +} + func appendRequestModifier(modifier requestModifier) doRequestOption { return func(opts *doRequestOptions) { opts.requestModifiers = append(opts.requestModifiers, modifier) } } +func withUrlQuery(key string, value any) doRequestOption { + return appendUrlModifier(func(url *url.URL) { + var valueStr string + if stringerValue, ok := value.(fmt.Stringer); ok { + valueStr = stringerValue.String() + } else { + valueStr = fmt.Sprintf("%v", value) + } + query := url.Query() + query.Set(key, valueStr) + url.RawQuery = query.Encode() + }) +} + func withAccept(accept string) doRequestOption { return withHeader("Accept", accept) } @@ -157,6 +181,18 @@ func (c *MeshStackProviderClient) doRequest(method string, url *url.URL, options option(&opts) } + if len(opts.urlModifiers) > 0 { + // clone url to prevent modifiers edit the given URL (it's sad that this is a pointer actually) + var err error + url, err = url.Parse(url.String()) + if err != nil { + panic("cloning URL failed: " + err.Error()) + } + for _, modifier := range opts.urlModifiers { + modifier(url) + } + } + var requestBody io.ReadWriter if opts.requestPayload != nil { requestBody = new(bytes.Buffer) @@ -217,6 +253,39 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url )...) } +func (c *MeshStackProviderClient) doPaginatedRequest(url *url.URL, options ...doRequestOption) iter.Seq2[[]byte, error] { + return func(yield func([]byte, error) bool) { + pageNumber := 0 + for { + body, err := c.doAuthenticatedRequest("GET", url, append(options, withUrlQuery("page", pageNumber))...) + if err != nil { + yield(body, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err)) + return + } + if !yield(body, nil) { + // consumer wants to stop + return + } + // Check if there are more pages to fetch + type paginatedResponse struct { + Page struct { + TotalPages int `json:"totalPages"` + Number int `json:"number"` + } `json:"page"` + } + response, err := unmarshalBody[paginatedResponse](body, err) + if err != nil { + yield(body, fmt.Errorf("cannot unmarshal paginated response, page %d: %w", pageNumber, err)) + return + } + if response.Page.Number >= response.Page.TotalPages-1 { + return + } + pageNumber++ + } + } +} + func unmarshalBody[T any](body []byte, err error) (*T, error) { if err != nil { return nil, err @@ -235,26 +304,18 @@ func unmarshalBodyIfPresent[T any](body []byte, err error) (*T, error) { return unmarshalBody[T](body, err) } -// paginatedResponse is a generic structure for HAL paginated responses -type paginatedResponse[T any] struct { - Embedded map[string][]T `json:"_embedded"` - Page struct { - Size int `json:"size"` - TotalElements int `json:"totalElements"` - TotalPages int `json:"totalPages"` - Number int `json:"number"` - } `json:"page"` -} - -// unmarshalPaginatedBody unmarshalls a paginated HAL response and extracts items using the provided key -func unmarshalPaginatedBody[T any](body []byte, err error, embeddedKey string) ([]T, *paginatedResponse[T], error) { - if err != nil { - return nil, nil, err - } - var response paginatedResponse[T] - if err := json.Unmarshal(body, &response); err != nil { - return nil, nil, err +func unmarshalBodyPages[T any](embeddedKey string, bodyPages iter.Seq2[[]byte, error]) (result []T, err error) { + for bodyPage, err := range bodyPages { + type embeddedResponse[T any] struct { + Embedded map[string][]T `json:"_embedded"` + } + if response, err := unmarshalBody[embeddedResponse[T]](bodyPage, err); err != nil { + return result, err + } else if items, ok := response.Embedded[embeddedKey]; !ok { + return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) + } else { + result = append(result, items...) + } } - items := response.Embedded[embeddedKey] - return items, &response, nil + return result, nil } diff --git a/integrations.go b/integrations.go index a495046..99f351d 100644 --- a/integrations.go +++ b/integrations.go @@ -1,7 +1,6 @@ package client import ( - "fmt" "net/url" ) @@ -96,34 +95,6 @@ func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) )) } -func (c *MeshStackProviderClient) ReadIntegrations() (*[]MeshIntegration, error) { - var allIntegrations []MeshIntegration - - pageNumber := 0 - targetUrl := c.endpoints.Integrations - query := targetUrl.Query() - - for { - query.Set("page", fmt.Sprintf("%d", pageNumber)) - targetUrl.RawQuery = query.Encode() - - body, err := c.doAuthenticatedRequest("GET", targetUrl, - withAccept(CONTENT_TYPE_INTEGRATION), - ) - items, response, err := unmarshalPaginatedBody[MeshIntegration](body, err, "meshIntegrations") - if err != nil { - return nil, err - } - - allIntegrations = append(allIntegrations, items...) - - // Check if there are more pages - if response.Page.Number >= response.Page.TotalPages-1 { - break - } - - pageNumber++ - } - - return &allIntegrations, nil +func (c *MeshStackProviderClient) ReadIntegrations() ([]MeshIntegration, error) { + return unmarshalBodyPages[MeshIntegration]("meshIntegrations", c.doPaginatedRequest(c.endpoints.Integrations, withAccept(CONTENT_TYPE_INTEGRATION))) } diff --git a/project.go b/project.go index 3138794..8a39499 100644 --- a/project.go +++ b/project.go @@ -1,7 +1,6 @@ package client import ( - "fmt" "net/url" ) @@ -49,40 +48,15 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M )) } -func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, paymentMethodIdentifier *string) (*[]MeshProject, error) { - var allProjects []MeshProject - - pageNumber := 0 - targetUrl := c.endpoints.Projects - query := targetUrl.Query() - query.Set("workspaceIdentifier", workspaceIdentifier) - if paymentMethodIdentifier != nil { - query.Set("paymentIdentifier", *paymentMethodIdentifier) +func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { + options := []doRequestOption{ + withAccept(CONTENT_TYPE_PROJECT), + withUrlQuery("workspaceIdentifier", workspaceIdentifier), } - - for { - query.Set("page", fmt.Sprintf("%d", pageNumber)) - targetUrl.RawQuery = query.Encode() - - body, err := c.doAuthenticatedRequest("GET", targetUrl, - withAccept(CONTENT_TYPE_PROJECT), - ) - items, response, err := unmarshalPaginatedBody[MeshProject](body, err, "meshProjects") - if err != nil { - return nil, err - } - - allProjects = append(allProjects, items...) - - // Check if there are more pages - if response.Page.Number >= response.Page.TotalPages-1 { - break - } - - pageNumber++ + if paymentMethodIdentifier != nil { + options = append(options, withUrlQuery("paymentIdentifier", *paymentMethodIdentifier)) } - - return &allProjects, nil + return unmarshalBodyPages[MeshProject]("meshProjects", c.doPaginatedRequest(c.endpoints.Projects, options...)) } func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*MeshProject, error) { diff --git a/tag_definition.go b/tag_definition.go index ed34177..c5112ab 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -1,7 +1,6 @@ package client import ( - "fmt" "net/url" ) @@ -73,36 +72,8 @@ 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() - - body, err := c.doAuthenticatedRequest("GET", targetUrl, - withAccept(CONTENT_TYPE_TAG_DEFINITION), - ) - items, response, err := unmarshalPaginatedBody[MeshTagDefinition](body, err, "meshTagDefinitions") - if err != nil { - return nil, err - } - - all = append(all, items...) - - // Check if there are more pages - if response.Page.Number >= response.Page.TotalPages-1 { - break - } - - pageNumber++ - } - - return &all, nil +func (c *MeshStackProviderClient) ReadTagDefinitions() ([]MeshTagDefinition, error) { + return unmarshalBodyPages[MeshTagDefinition]("meshTagDefinitions", c.doPaginatedRequest(c.endpoints.TagDefinitions, withAccept(CONTENT_TYPE_TAG_DEFINITION))) } func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefinition, error) { From b9fdd6f3ee708c60311803c7cc9e908101880676 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 22:09:54 +0100 Subject: [PATCH 072/200] refactor: use generic meshObjectClient to build MeshStackProviderClient, add data source tests --- buildingblock.go | 29 ++---- buildingblock_v2.go | 49 +++++----- client.go | 190 ++++++++++++++++++++++--------------- integrations.go | 24 ++--- landingzone.go | 35 ++----- location.go | 35 ++----- payment_method.go | 35 ++----- platform.go | 35 ++----- project.go | 45 ++++----- project_group_binding.go | 29 ++---- project_user_binding.go | 29 ++---- tag_definition.go | 38 +++----- tenant.go | 34 +++---- tenant_v4.go | 50 ++++------ workspace.go | 35 ++----- workspace_group_binding.go | 29 ++---- workspace_user_binding.go | 29 ++---- 17 files changed, 297 insertions(+), 453 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index f807a17..19f4f44 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -1,9 +1,5 @@ package client -import ( - "net/url" -) - const ( MESH_BUILDING_BLOCK_IO_TYPE_STRING = "STRING" MESH_BUILDING_BLOCK_IO_TYPE_INTEGER = "INTEGER" @@ -13,8 +9,6 @@ const ( MESH_BUILDING_BLOCK_IO_TYPE_FILE = "FILE" MESH_BUILDING_BLOCK_IO_TYPE_LIST = "LIST" MESH_BUILDING_BLOCK_IO_TYPE_CODE = "CODE" - - CONTENT_TYPE_BUILDING_BLOCK = "application/vnd.meshcloud.api.meshbuildingblock.v1.hal+json" ) type MeshBuildingBlock struct { @@ -76,25 +70,18 @@ type MeshBuildingBlockDefinitionRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` } -func (c *MeshStackProviderClient) urlForBuildingBlock(uuid string) *url.URL { - return c.endpoints.BuildingBlocks.JoinPath(uuid) +type MeshBuildingBlockClient struct { + meshObjectClient[MeshBuildingBlock] } -func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { - return unmarshalBodyIfPresent[MeshBuildingBlock](c.doAuthenticatedRequest("GET", c.urlForBuildingBlock(uuid), - withAccept(CONTENT_TYPE_BUILDING_BLOCK), - )) +func (c MeshBuildingBlockClient) Read(uuid string) (*MeshBuildingBlock, error) { + return c.get(uuid) } -func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { - return unmarshalBody[MeshBuildingBlock](c.doAuthenticatedRequest("POST", c.endpoints.BuildingBlocks, - withPayload(bb, CONTENT_TYPE_BUILDING_BLOCK), - )) +func (c MeshBuildingBlockClient) Create(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { + return c.post(bb) } -func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid), - withAccept(CONTENT_TYPE_BUILDING_BLOCK), - ) - return err +func (c MeshBuildingBlockClient) Delete(uuid string) error { + return c.delete(uuid) } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 6f86a1b..b767b76 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -9,8 +9,6 @@ import ( ) const ( - CONTENT_TYPE_BUILDING_BLOCK_V2 = "application/vnd.meshcloud.api.meshbuildingblock.v2-preview.hal+json" - // Building Block Status Constants. BUILDING_BLOCK_STATUS_WAITING_FOR_DEPENDENT_INPUT = "WAITING_FOR_DEPENDENT_INPUT" BUILDING_BLOCK_STATUS_WAITING_FOR_OPERATOR_INPUT = "WAITING_FOR_OPERATOR_INPUT" @@ -67,38 +65,35 @@ type MeshBuildingBlockV2Status struct { ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` } -func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildingBlockV2, error) { - return unmarshalBodyIfPresent[MeshBuildingBlockV2](c.doAuthenticatedRequest("GET", c.urlForBuildingBlock(uuid), - withAccept(CONTENT_TYPE_BUILDING_BLOCK_V2), - )) +type MeshBuildingBlockV2Client struct { + meshObjectClient[MeshBuildingBlockV2] +} + +func (c MeshBuildingBlockV2Client) Read(uuid string) (*MeshBuildingBlockV2, error) { + return c.get(uuid) } -func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { - return unmarshalBody[MeshBuildingBlockV2](c.doAuthenticatedRequest("POST", c.endpoints.BuildingBlocks, - withPayload(bb, CONTENT_TYPE_BUILDING_BLOCK_V2), - )) +func (c MeshBuildingBlockV2Client) Create(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { + return c.post(bb) } -func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForBuildingBlock(uuid), - withAccept(CONTENT_TYPE_BUILDING_BLOCK_V2), - ) - return err +func (c MeshBuildingBlockV2Client) Delete(uuid string) error { + return c.delete(uuid) } -// PollBuildingBlockV2UntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) +// PollUntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) // Returns the final building block state or an error if polling fails or times out. -func (c *MeshStackProviderClient) PollBuildingBlockV2UntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { +func (c MeshBuildingBlockV2Client) PollUntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { var result *MeshBuildingBlockV2 - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2CompletionFunc(uuid, &result)) + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCompletionFunc(uuid, &result)) return result, err } -// waitForBuildingBlockV2CompletionFunc returns a RetryFunc that checks building block completion status. -func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { +// waitForCompletionFunc returns a RetryFunc that checks building block completion status. +func (c MeshBuildingBlockV2Client) waitForCompletionFunc(uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.ReadBuildingBlockV2(uuid) + current, err := c.Read(uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for completion: %w", err)) } @@ -122,16 +117,16 @@ func (c *MeshStackProviderClient) waitForBuildingBlockV2CompletionFunc(uuid stri } } -// PollBuildingBlockV2UntilDeletion polls a building block until it is deleted (not found) +// PollUntilDeletion polls a building block until it is deleted (not found) // Returns nil on successful deletion or an error if polling fails or times out. -func (c *MeshStackProviderClient) PollBuildingBlockV2UntilDeletion(ctx context.Context, uuid string) error { - return retry.RetryContext(ctx, 30*time.Minute, c.waitForBuildingBlockV2DeletionFunc(uuid)) +func (c MeshBuildingBlockV2Client) PollUntilDeletion(ctx context.Context, uuid string) error { + return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(uuid)) } -// waitForBuildingBlockV2DeletionFunc returns a RetryFunc that checks building block deletion status. -func (c *MeshStackProviderClient) waitForBuildingBlockV2DeletionFunc(uuid string) retry.RetryFunc { +// waitForDeletionFunc returns a RetryFunc that checks building block deletion status. +func (c MeshBuildingBlockV2Client) waitForDeletionFunc(uuid string) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.ReadBuildingBlockV2(uuid) + current, err := c.Read(uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for deletion: %w", err)) } diff --git a/client.go b/client.go index 3a56804..84cd423 100644 --- a/client.go +++ b/client.go @@ -11,6 +11,7 @@ import ( "net/http" "net/url" "slices" + "strings" "time" ) @@ -19,69 +20,110 @@ var ( ) type MeshStackProviderClient struct { - url *url.URL - httpClient *http.Client - apiKey string - apiSecret string - token string - tokenExpiry time.Time - endpoints endpoints + BuildingBlock MeshBuildingBlockClient + BuildingBlockV2 MeshBuildingBlockV2Client + Integration MeshIntegrationClient + LandingZone MeshLandingZoneClient + Location MeshLocationClient + PaymentMethod MeshPaymentMethodClient + Platform MeshPlatformClient + Project MeshProjectClient + ProjectGroupBinding MeshProjectGroupBindingClient + ProjectUserBinding MeshProjectUserBindingClient + TagDefinition MeshTagDefinitionClient + Tenant MeshTenantClient + TenantV4 MeshTenantV4Client + Workspace MeshWorkspaceClient + WorkspaceGroupBinding MeshWorkspaceGroupBindingClient + WorkspaceUserBinding MeshWorkspaceUserBindingClient } -type endpoints struct { - BuildingBlocks *url.URL `json:"meshbuildingblocks"` - Projects *url.URL `json:"meshprojects"` - ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` - ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` - Workspaces *url.URL `json:"meshworkspaces"` - WorkspaceUserBindings *url.URL `json:"meshworkspaceuserbindings"` - WorkspaceGroupBindings *url.URL `json:"meshworkspacegroupbindings"` - Tenants *url.URL `json:"meshtenants"` - TagDefinitions *url.URL `json:"meshtagdefinitions"` - LandingZones *url.URL `json:"meshlandingzones"` - Platforms *url.URL `json:"meshplatforms"` - PaymentMethods *url.URL `json:"meshpaymentmethods"` - Integrations *url.URL `json:"meshintegrations"` - Locations *url.URL `json:"meshlocations"` +func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) MeshStackProviderClient { + // Initialize httpClient for typed clients + c := &httpClient{ + Client: http.Client{Timeout: 5 * time.Minute}, + RootUrl: rootUrl, + ApiKey: apiKey, + ApiSecret: apiSecret, + } + return MeshStackProviderClient{ + MeshBuildingBlockClient{newMeshObjectClient[MeshBuildingBlock](c, "meshBuildingBlock", "v1")}, + MeshBuildingBlockV2Client{newMeshObjectClient[MeshBuildingBlockV2](c, "meshBuildingBlock", "v2-preview")}, + MeshIntegrationClient{newMeshObjectClient[MeshIntegration](c, "meshIntegration", "v1-preview")}, + MeshLandingZoneClient{newMeshObjectClient[MeshLandingZone](c, "meshLandingZone", "v1-preview")}, + MeshLocationClient{newMeshObjectClient[MeshLocation](c, "meshLocation", "v1-preview")}, + MeshPaymentMethodClient{newMeshObjectClient[MeshPaymentMethod](c, "meshPaymentMethod", "v2")}, + MeshPlatformClient{newMeshObjectClient[MeshPlatform](c, "meshPlatform", "v2-preview")}, + MeshProjectClient{newMeshObjectClient[MeshProject](c, "meshProject", "v2")}, + MeshProjectGroupBindingClient{newMeshObjectClient[MeshProjectBinding](c, "meshProjectGroupBinding", "v3", "meshprojectbindings", "groupbindings")}, + MeshProjectUserBindingClient{newMeshObjectClient[MeshProjectBinding](c, "meshProjectUserBinding", "v3", "meshprojectbindings", "userbindings")}, + MeshTagDefinitionClient{newMeshObjectClient[MeshTagDefinition](c, "meshTagDefinition", "v1")}, + MeshTenantClient{newMeshObjectClient[MeshTenant](c, "meshTenant", "v3")}, + MeshTenantV4Client{newMeshObjectClient[MeshTenantV4](c, "meshTenant", "v4-preview")}, + MeshWorkspaceClient{newMeshObjectClient[MeshWorkspace](c, "meshWorkspace", "v2")}, + MeshWorkspaceGroupBindingClient{newMeshObjectClient[MeshWorkspaceBinding](c, "meshWorkspaceGroupBinding", "v2", "meshworkspacebindings", "groupbindings")}, + MeshWorkspaceUserBindingClient{newMeshObjectClient[MeshWorkspaceBinding](c, "meshWorkspaceUserBinding", "v2", "meshworkspacebindings", "userbindings")}, + } } -func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackProviderClient, error) { - client := &MeshStackProviderClient{ - url: rootUrl, - httpClient: &http.Client{ - Timeout: time.Minute * 5, - }, - apiKey: apiKey, - apiSecret: apiSecret, - token: "", - } +type httpClient struct { + http.Client + RootUrl *url.URL + ApiKey string + ApiSecret string + Token string + TokenExpiry time.Time +} - // TODO: lookup endpoints - const ( - apiMeshObjectsRoot = "/api/meshobjects" - ) - client.endpoints = endpoints{ - BuildingBlocks: rootUrl.JoinPath(apiMeshObjectsRoot, "meshbuildingblocks"), - Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), - ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), - ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), - Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), - WorkspaceUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "userbindings"), - WorkspaceGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspacebindings", "groupbindings"), - Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), - TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), - LandingZones: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlandingzones"), - Platforms: rootUrl.JoinPath(apiMeshObjectsRoot, "meshplatforms"), - PaymentMethods: rootUrl.JoinPath(apiMeshObjectsRoot, "meshpaymentmethods"), - Integrations: rootUrl.JoinPath(apiMeshObjectsRoot, "meshintegrations"), - Locations: rootUrl.JoinPath(apiMeshObjectsRoot, "meshlocations"), +type meshObjectClient[M any] struct { + *httpClient + Name, ApiVersion string + ApiUrl *url.URL +} + +func newMeshObjectClient[M any](client *httpClient, name, apiVersion string, explicitApiPaths ...string) meshObjectClient[M] { + if len(explicitApiPaths) == 0 { + // infer API path from meshObject name by default (if nothing explicit is given) + explicitApiPaths = []string{strings.ToLower(pluralizeName(name))} } + // also prepend the root path for all meshObjects + explicitApiPaths = slices.Insert(explicitApiPaths, 0, "/api/meshobjects") + apiUrl := client.RootUrl.JoinPath(explicitApiPaths...) + log.Printf("Using API at '%s' for meshObject '%s', version '%s'", apiUrl, name, apiVersion) + return meshObjectClient[M]{client, name, apiVersion, apiUrl} +} - return client, nil +func pluralizeName(name string) string { + return fmt.Sprintf("%ss", name) } -func (c *MeshStackProviderClient) login() error { - loginUrl := c.url.JoinPath("/api/login") +func (o meshObjectClient[M]) mediaType() string { + return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", o.Name, o.ApiVersion) +} + +func (c meshObjectClient[M]) get(id string) (*M, error) { + return unmarshalBodyIfPresent[M](c.doAuthenticatedRequest(http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.mediaType()))) +} + +func (c meshObjectClient[M]) list(options ...doRequestOption) ([]M, error) { + return unmarshalBodyPages[M](pluralizeName(c.Name), c.doPaginatedRequest(c.ApiUrl, append(options, withAccept(c.mediaType()))...)) +} + +func (c meshObjectClient[M]) post(payload any) (*M, error) { + return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPost, c.ApiUrl, withPayload(payload, c.mediaType()))) +} + +func (c meshObjectClient[M]) put(id string, payload any) (*M, error) { + return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.mediaType()))) +} + +func (c meshObjectClient[M]) delete(id string) (err error) { + _, err = c.doAuthenticatedRequest(http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.mediaType())) + return +} + +func (c *httpClient) login() error { + loginApiUrl := c.RootUrl.JoinPath("/api/login") type loginRequest struct { ClientId string `json:"clientId"` @@ -93,20 +135,20 @@ func (c *MeshStackProviderClient) login() error { ExpireSec int `json:"expires_in"` } - loginResult, err := unmarshalBody[loginResponse](c.doRequest("POST", loginUrl, - withPayload(loginRequest{ClientId: c.apiKey, ClientSecret: c.apiSecret}, "application/json")), + loginResult, err := unmarshalBody[loginResponse](c.doRequest("POST", loginApiUrl, + withPayload(loginRequest{ClientId: c.ApiKey, ClientSecret: c.ApiSecret}, "application/json")), ) if err != nil { - return fmt.Errorf("login request to %s with API Key '%s' failed: %w", loginUrl, c.apiKey, err) + return fmt.Errorf("login request to %s with API Key '%s' failed: %w", loginApiUrl, c.ApiKey, err) } - c.token = fmt.Sprintf("Bearer %s", loginResult.Token) - c.tokenExpiry = time.Now().Add(time.Second * time.Duration(loginResult.ExpireSec)) + c.Token = fmt.Sprintf("Bearer %s", loginResult.Token) + c.TokenExpiry = time.Now().Add(time.Second * time.Duration(loginResult.ExpireSec)) return nil } -func (c *MeshStackProviderClient) ensureValidToken() error { - if c.token == "" || time.Now().Add(time.Second*30).After(c.tokenExpiry) { +func (c *httpClient) ensureValidToken() error { + if c.Token == "" || time.Now().Add(30*time.Second).After(c.TokenExpiry) { return c.login() } return nil @@ -171,7 +213,7 @@ func withPayload(payload any, contentType string) doRequestOption { } } -func (c *MeshStackProviderClient) doRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { +func (c *httpClient) doRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { // prepend (aka insert at 0) some default options such that given options may be overridden by caller options = slices.Insert(options, 0, withHeader("User-Agent", "meshStack Terraform Provider"), @@ -183,11 +225,8 @@ func (c *MeshStackProviderClient) doRequest(method string, url *url.URL, options if len(opts.urlModifiers) > 0 { // clone url to prevent modifiers edit the given URL (it's sad that this is a pointer actually) - var err error - url, err = url.Parse(url.String()) - if err != nil { - panic("cloning URL failed: " + err.Error()) - } + // ignoring the error is fine as this always succeeds parsing from String() + url, _ = url.Parse(url.String()) for _, modifier := range opts.urlModifiers { modifier(url) } @@ -209,7 +248,7 @@ func (c *MeshStackProviderClient) doRequest(method string, url *url.URL, options requestModifier(req) } - res, err := c.httpClient.Do(req) + res, err := c.Do(req) if err != nil { return nil, err } @@ -240,7 +279,7 @@ func (c *MeshStackProviderClient) doRequest(method string, url *url.URL, options return responseBody, errors.Join(errs...) } -func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { +func (c *httpClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { if err := c.ensureValidToken(); err != nil { return nil, err } @@ -249,15 +288,15 @@ func (c *MeshStackProviderClient) doAuthenticatedRequest(method string, url *url // log request before adding Authorization header below log.Println(req) }), - withHeader("Authorization", c.token), + withHeader("Authorization", c.Token), )...) } -func (c *MeshStackProviderClient) doPaginatedRequest(url *url.URL, options ...doRequestOption) iter.Seq2[[]byte, error] { +func (c *httpClient) doPaginatedRequest(url *url.URL, options ...doRequestOption) iter.Seq2[[]byte, error] { return func(yield func([]byte, error) bool) { pageNumber := 0 for { - body, err := c.doAuthenticatedRequest("GET", url, append(options, withUrlQuery("page", pageNumber))...) + body, err := c.doAuthenticatedRequest(http.MethodGet, url, append(options, withUrlQuery("page", pageNumber))...) if err != nil { yield(body, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err)) return @@ -304,12 +343,13 @@ func unmarshalBodyIfPresent[T any](body []byte, err error) (*T, error) { return unmarshalBody[T](body, err) } -func unmarshalBodyPages[T any](embeddedKey string, bodyPages iter.Seq2[[]byte, error]) (result []T, err error) { - for bodyPage, err := range bodyPages { +func unmarshalBodyPages[T any](embeddedKey string, bodyPages iter.Seq2[[]byte, error]) ([]T, error) { + var result []T + for bodyPage, pageErr := range bodyPages { type embeddedResponse[T any] struct { Embedded map[string][]T `json:"_embedded"` } - if response, err := unmarshalBody[embeddedResponse[T]](bodyPage, err); err != nil { + if response, err := unmarshalBody[embeddedResponse[T]](bodyPage, pageErr); err != nil { return result, err } else if items, ok := response.Embedded[embeddedKey]; !ok { return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) diff --git a/integrations.go b/integrations.go index 99f351d..9aa514a 100644 --- a/integrations.go +++ b/integrations.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_INTEGRATION = "application/vnd.meshcloud.api.meshintegration.v1-preview.hal+json" - type MeshIntegration struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -85,16 +79,18 @@ type MeshAwsWifProvider struct { Thumbprint string `json:"thumbprint" tfsdk:"thumbprint"` } -func (c *MeshStackProviderClient) urlForIntegration(workspace string, uuid string) *url.URL { - return c.endpoints.Integrations.JoinPath(workspace, uuid) +type MeshIntegrationClient struct { + meshObjectClient[MeshIntegration] +} + +func (c MeshIntegrationClient) integrationId(workspace string, uuid string) string { + return workspace + "/" + uuid } -func (c *MeshStackProviderClient) ReadIntegration(workspace string, uuid string) (*MeshIntegration, error) { - return unmarshalBodyIfPresent[MeshIntegration](c.doAuthenticatedRequest("GET", c.urlForIntegration(workspace, uuid), - withAccept(CONTENT_TYPE_INTEGRATION), - )) +func (c MeshIntegrationClient) Read(workspace string, uuid string) (*MeshIntegration, error) { + return c.get(c.integrationId(workspace, uuid)) } -func (c *MeshStackProviderClient) ReadIntegrations() ([]MeshIntegration, error) { - return unmarshalBodyPages[MeshIntegration]("meshIntegrations", c.doPaginatedRequest(c.endpoints.Integrations, withAccept(CONTENT_TYPE_INTEGRATION))) +func (c MeshIntegrationClient) List() ([]MeshIntegration, error) { + return c.list() } diff --git a/landingzone.go b/landingzone.go index c5f3ca9..0967a8c 100644 --- a/landingzone.go +++ b/landingzone.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_LANDINGZONE = "application/vnd.meshcloud.api.meshlandingzone.v1-preview.hal+json" - type MeshLandingZone struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -65,31 +59,22 @@ type MeshLandingZoneCreate struct { Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` } -func (c *MeshStackProviderClient) urlForLandingZone(name string) *url.URL { - return c.endpoints.LandingZones.JoinPath(name) +type MeshLandingZoneClient struct { + meshObjectClient[MeshLandingZone] } -func (c *MeshStackProviderClient) ReadLandingZone(name string) (*MeshLandingZone, error) { - return unmarshalBodyIfPresent[MeshLandingZone](c.doAuthenticatedRequest("GET", c.urlForLandingZone(name), - withAccept(CONTENT_TYPE_LANDINGZONE), - )) +func (c MeshLandingZoneClient) Read(name string) (*MeshLandingZone, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateLandingZone(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest("POST", c.endpoints.LandingZones, - withPayload(landingZone, CONTENT_TYPE_LANDINGZONE), - )) +func (c MeshLandingZoneClient) Create(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { + return c.post(landingZone) } -func (c *MeshStackProviderClient) UpdateLandingZone(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - return unmarshalBody[MeshLandingZone](c.doAuthenticatedRequest("PUT", c.urlForLandingZone(name), - withPayload(landingZone, CONTENT_TYPE_LANDINGZONE), - )) +func (c MeshLandingZoneClient) Update(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { + return c.put(name, landingZone) } -func (c *MeshStackProviderClient) DeleteLandingZone(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForLandingZone(name), - withAccept(CONTENT_TYPE_LANDINGZONE), - ) - return err +func (c MeshLandingZoneClient) Delete(name string) error { + return c.delete(name) } diff --git a/location.go b/location.go index e0a6474..8182416 100644 --- a/location.go +++ b/location.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_LOCATION = "application/vnd.meshcloud.api.meshlocation.v1-preview.hal+json" - type MeshLocation struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Metadata MeshLocationMetadata `json:"metadata" tfsdk:"metadata"` @@ -37,31 +31,22 @@ type MeshLocationCreateMetadata struct { Name string `json:"name" tfsdk:"name"` } -func (c *MeshStackProviderClient) urlForLocation(name string) *url.URL { - return c.endpoints.Locations.JoinPath(name) +type MeshLocationClient struct { + meshObjectClient[MeshLocation] } -func (c *MeshStackProviderClient) ReadLocation(name string) (*MeshLocation, error) { - return unmarshalBodyIfPresent[MeshLocation](c.doAuthenticatedRequest("GET", c.urlForLocation(name), - withAccept(CONTENT_TYPE_LOCATION), - )) +func (c MeshLocationClient) Read(name string) (*MeshLocation, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateLocation(location *MeshLocationCreate) (*MeshLocation, error) { - return unmarshalBody[MeshLocation](c.doAuthenticatedRequest("POST", c.endpoints.Locations, - withPayload(location, CONTENT_TYPE_LOCATION), - )) +func (c MeshLocationClient) Create(location *MeshLocationCreate) (*MeshLocation, error) { + return c.post(location) } -func (c *MeshStackProviderClient) UpdateLocation(name string, location *MeshLocationCreate) (*MeshLocation, error) { - return unmarshalBody[MeshLocation](c.doAuthenticatedRequest("PUT", c.urlForLocation(name), - withPayload(location, CONTENT_TYPE_LOCATION), - )) +func (c MeshLocationClient) Update(name string, location *MeshLocationCreate) (*MeshLocation, error) { + return c.put(name, location) } -func (c *MeshStackProviderClient) DeleteLocation(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForLocation(name), - withAccept(CONTENT_TYPE_LOCATION), - ) - return err +func (c MeshLocationClient) Delete(name string) error { + return c.delete(name) } diff --git a/payment_method.go b/payment_method.go index 0649836..84f73f2 100644 --- a/payment_method.go +++ b/payment_method.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_PAYMENT_METHOD = "application/vnd.meshcloud.api.meshpaymentmethod.v2.hal+json" - type MeshPaymentMethod struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -38,31 +32,22 @@ type MeshPaymentMethodCreateMetadata struct { OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -func (c *MeshStackProviderClient) urlForPaymentMethod(identifier string) *url.URL { - return c.endpoints.PaymentMethods.JoinPath(identifier) +type MeshPaymentMethodClient struct { + meshObjectClient[MeshPaymentMethod] } -func (c *MeshStackProviderClient) ReadPaymentMethod(workspace string, identifier string) (*MeshPaymentMethod, error) { - return unmarshalBodyIfPresent[MeshPaymentMethod](c.doAuthenticatedRequest("GET", c.urlForPaymentMethod(identifier), - withAccept(CONTENT_TYPE_PAYMENT_METHOD), - )) +func (c MeshPaymentMethodClient) Read(workspace string, identifier string) (*MeshPaymentMethod, error) { + return c.get(identifier) } -func (c *MeshStackProviderClient) CreatePaymentMethod(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest("POST", c.endpoints.PaymentMethods, - withPayload(paymentMethod, CONTENT_TYPE_PAYMENT_METHOD), - )) +func (c MeshPaymentMethodClient) Create(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + return c.post(paymentMethod) } -func (c *MeshStackProviderClient) UpdatePaymentMethod(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - return unmarshalBody[MeshPaymentMethod](c.doAuthenticatedRequest("PUT", c.urlForPaymentMethod(identifier), - withPayload(paymentMethod, CONTENT_TYPE_PAYMENT_METHOD), - )) +func (c MeshPaymentMethodClient) Update(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + return c.put(identifier, paymentMethod) } -func (c *MeshStackProviderClient) DeletePaymentMethod(identifier string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPaymentMethod(identifier), - withAccept(CONTENT_TYPE_PAYMENT_METHOD), - ) - return err +func (c MeshPaymentMethodClient) Delete(identifier string) error { + return c.delete(identifier) } diff --git a/platform.go b/platform.go index e4d0081..cc32525 100644 --- a/platform.go +++ b/platform.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_PLATFORM = "application/vnd.meshcloud.api.meshplatform.v2-preview.hal+json" - type MeshPlatform struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -109,31 +103,22 @@ type TagMapper struct { ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` } -func (c *MeshStackProviderClient) urlForPlatform(uuid string) *url.URL { - return c.endpoints.Platforms.JoinPath(uuid) +type MeshPlatformClient struct { + meshObjectClient[MeshPlatform] } -func (c *MeshStackProviderClient) ReadPlatform(uuid string) (*MeshPlatform, error) { - return unmarshalBodyIfPresent[MeshPlatform](c.doAuthenticatedRequest("GET", c.urlForPlatform(uuid), - withAccept(CONTENT_TYPE_PLATFORM), - )) +func (c MeshPlatformClient) Read(uuid string) (*MeshPlatform, error) { + return c.get(uuid) } -func (c *MeshStackProviderClient) CreatePlatform(platform *MeshPlatformCreate) (*MeshPlatform, error) { - return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest("POST", c.endpoints.Platforms, - withPayload(platform, CONTENT_TYPE_PLATFORM), - )) +func (c MeshPlatformClient) Create(platform *MeshPlatformCreate) (*MeshPlatform, error) { + return c.post(platform) } -func (c *MeshStackProviderClient) DeletePlatform(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPlatform(uuid), - withAccept(CONTENT_TYPE_PLATFORM), - ) - return err +func (c MeshPlatformClient) Update(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { + return c.put(uuid, platform) } -func (c *MeshStackProviderClient) UpdatePlatform(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { - return unmarshalBody[MeshPlatform](c.doAuthenticatedRequest("PUT", c.urlForPlatform(uuid), - withPayload(platform, CONTENT_TYPE_PLATFORM), - )) +func (c MeshPlatformClient) Delete(uuid string) error { + return c.delete(uuid) } diff --git a/project.go b/project.go index 8a39499..cb509b2 100644 --- a/project.go +++ b/project.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_PROJECT = "application/vnd.meshcloud.api.meshproject.v2.hal+json" - type MeshProject struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -37,43 +31,36 @@ type MeshProjectCreateMetadata struct { OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -func (c *MeshStackProviderClient) urlForProject(workspace string, name string) *url.URL { - identifier := workspace + "." + name - return c.endpoints.Projects.JoinPath(identifier) +type MeshProjectClient struct { + meshObjectClient[MeshProject] +} + +func (c *MeshProjectClient) projectId(workspace string, name string) string { + return workspace + "." + name } -func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*MeshProject, error) { - return unmarshalBodyIfPresent[MeshProject](c.doAuthenticatedRequest("GET", c.urlForProject(workspace, name), - withAccept(CONTENT_TYPE_PROJECT), - )) +func (c *MeshProjectClient) Read(workspace string, name string) (*MeshProject, error) { + return c.get(c.projectId(workspace, name)) } -func (c *MeshStackProviderClient) ReadProjects(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { +func (c *MeshProjectClient) List(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { options := []doRequestOption{ - withAccept(CONTENT_TYPE_PROJECT), withUrlQuery("workspaceIdentifier", workspaceIdentifier), } if paymentMethodIdentifier != nil { options = append(options, withUrlQuery("paymentIdentifier", *paymentMethodIdentifier)) } - return unmarshalBodyPages[MeshProject]("meshProjects", c.doPaginatedRequest(c.endpoints.Projects, options...)) + return c.list(options...) } -func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*MeshProject, error) { - return unmarshalBody[MeshProject](c.doAuthenticatedRequest("POST", c.endpoints.Projects, - withPayload(project, CONTENT_TYPE_PROJECT), - )) +func (c *MeshProjectClient) Create(project *MeshProjectCreate) (*MeshProject, error) { + return c.post(project) } -func (c *MeshStackProviderClient) UpdateProject(project *MeshProjectCreate) (*MeshProject, error) { - return unmarshalBody[MeshProject](c.doAuthenticatedRequest("PUT", c.urlForProject(project.Metadata.OwnedByWorkspace, project.Metadata.Name), - withPayload(project, CONTENT_TYPE_PROJECT), - )) +func (c *MeshProjectClient) Update(project *MeshProjectCreate) (*MeshProject, error) { + return c.put(c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) } -func (c *MeshStackProviderClient) DeleteProject(workspace string, name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForProject(workspace, name), - withAccept(CONTENT_TYPE_PROJECT), - ) - return err +func (c *MeshProjectClient) Delete(workspace string, name string) error { + return c.delete(c.projectId(workspace, name)) } diff --git a/project_group_binding.go b/project_group_binding.go index 4737824..96d6640 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -1,32 +1,19 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_PROJECT_GROUP_BINDING = "application/vnd.meshcloud.api.meshprojectgroupbinding.v3.hal+json" - type MeshProjectGroupBinding = MeshProjectBinding -func (c *MeshStackProviderClient) urlForPojectGroupBinding(name string) *url.URL { - return c.endpoints.ProjectGroupBindings.JoinPath(name) +type MeshProjectGroupBindingClient struct { + meshObjectClient[MeshProjectBinding] } -func (c *MeshStackProviderClient) ReadProjectGroupBinding(name string) (*MeshProjectGroupBinding, error) { - return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest("GET", c.urlForPojectGroupBinding(name), - withAccept(CONTENT_TYPE_PROJECT_GROUP_BINDING), - )) +func (c MeshProjectGroupBindingClient) Read(name string) (*MeshProjectGroupBinding, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateProjectGroupBinding(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { - return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest("POST", c.endpoints.ProjectGroupBindings, - withPayload(binding, CONTENT_TYPE_PROJECT_GROUP_BINDING), - )) +func (c MeshProjectGroupBindingClient) Create(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { + return c.post(binding) } -func (c *MeshStackProviderClient) DeleteProjecGroupBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectGroupBinding(name), - withAccept(CONTENT_TYPE_PROJECT_GROUP_BINDING), - ) - return err +func (c MeshProjectGroupBindingClient) Delete(name string) error { + return c.delete(name) } diff --git a/project_user_binding.go b/project_user_binding.go index 9ade8d5..ee619d3 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -1,32 +1,19 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_PROJECT_USER_BINDING = "application/vnd.meshcloud.api.meshprojectuserbinding.v3.hal+json" - type MeshProjectUserBinding = MeshProjectBinding -func (c *MeshStackProviderClient) urlForPojectUserBinding(name string) *url.URL { - return c.endpoints.ProjectUserBindings.JoinPath(name) +type MeshProjectUserBindingClient struct { + meshObjectClient[MeshProjectBinding] } -func (c *MeshStackProviderClient) ReadProjectUserBinding(name string) (*MeshProjectUserBinding, error) { - return unmarshalBodyIfPresent[MeshProjectBinding](c.doAuthenticatedRequest("GET", c.urlForPojectUserBinding(name), - withAccept(CONTENT_TYPE_PROJECT_USER_BINDING), - )) +func (c MeshProjectUserBindingClient) Read(name string) (*MeshProjectUserBinding, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateProjectUserBinding(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - return unmarshalBody[MeshProjectBinding](c.doAuthenticatedRequest("POST", c.endpoints.ProjectUserBindings, - withPayload(binding, CONTENT_TYPE_PROJECT_USER_BINDING), - )) +func (c MeshProjectUserBindingClient) Create(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { + return c.post(binding) } -func (c *MeshStackProviderClient) DeleteProjecUserBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForPojectUserBinding(name), - withAccept(CONTENT_TYPE_PROJECT_USER_BINDING), - ) - return err +func (c MeshProjectUserBindingClient) Delete(name string) error { + return c.delete(name) } diff --git a/tag_definition.go b/tag_definition.go index c5112ab..884b324 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -1,11 +1,6 @@ package client -import ( - "net/url" -) - const API_VERSION_TAG_DEFINITION = "v1" -const CONTENT_TYPE_TAG_DEFINITION = "application/vnd.meshcloud.api.meshtagdefinition.v1.hal+json" type MeshTagDefinition struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` @@ -68,35 +63,26 @@ type TagValueMultiSelect struct { DefaultValue *[]string `json:"defaultValue,omitempty" tfsdk:"default_value"` } -func (c *MeshStackProviderClient) urlForTagDefinition(name string) *url.URL { - return c.endpoints.TagDefinitions.JoinPath(name) +type MeshTagDefinitionClient struct { + meshObjectClient[MeshTagDefinition] } -func (c *MeshStackProviderClient) ReadTagDefinitions() ([]MeshTagDefinition, error) { - return unmarshalBodyPages[MeshTagDefinition]("meshTagDefinitions", c.doPaginatedRequest(c.endpoints.TagDefinitions, withAccept(CONTENT_TYPE_TAG_DEFINITION))) +func (c MeshTagDefinitionClient) List() ([]MeshTagDefinition, error) { + return c.list() } -func (c *MeshStackProviderClient) ReadTagDefinition(name string) (*MeshTagDefinition, error) { - return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("GET", c.urlForTagDefinition(name), - withAccept(CONTENT_TYPE_TAG_DEFINITION), - )) +func (c MeshTagDefinitionClient) Read(name string) (*MeshTagDefinition, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("POST", c.endpoints.TagDefinitions, - withPayload(tagDefinition, CONTENT_TYPE_TAG_DEFINITION), - )) +func (c MeshTagDefinitionClient) Create(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { + return c.post(tagDefinition) } -func (c *MeshStackProviderClient) UpdateTagDefinition(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - return unmarshalBody[MeshTagDefinition](c.doAuthenticatedRequest("PUT", c.urlForTagDefinition(tagDefinition.Metadata.Name), - withPayload(tagDefinition, CONTENT_TYPE_TAG_DEFINITION), - )) +func (c MeshTagDefinitionClient) Update(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { + return c.put(tagDefinition.Metadata.Name, tagDefinition) } -func (c *MeshStackProviderClient) DeleteTagDefinition(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForTagDefinition(name), - withAccept(CONTENT_TYPE_TAG_DEFINITION), - ) - return err +func (c MeshTagDefinitionClient) Delete(name string) error { + return c.delete(name) } diff --git a/tenant.go b/tenant.go index b187f83..d1b5a32 100644 --- a/tenant.go +++ b/tenant.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_TENANT = "application/vnd.meshcloud.api.meshtenant.v3.hal+json" - type MeshTenant struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -49,26 +43,22 @@ type MeshTenantCreateSpec struct { Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } -func (c *MeshStackProviderClient) urlForTenant(workspace string, project string, platform string) *url.URL { - identifier := workspace + "." + project + "." + platform - return c.endpoints.Tenants.JoinPath(identifier) +type MeshTenantClient struct { + meshObjectClient[MeshTenant] +} + +func (c *MeshTenantClient) tenantId(workspace string, project string, platform string) string { + return workspace + "." + project + "." + platform } -func (c *MeshStackProviderClient) ReadTenant(workspace string, project string, platform string) (*MeshTenant, error) { - return unmarshalBodyIfPresent[MeshTenant](c.doAuthenticatedRequest("GET", c.urlForTenant(workspace, project, platform), - withAccept(CONTENT_TYPE_TENANT), - )) +func (c *MeshTenantClient) Read(workspace string, project string, platform string) (*MeshTenant, error) { + return c.get(c.tenantId(workspace, project, platform)) } -func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshTenant, error) { - return unmarshalBody[MeshTenant](c.doAuthenticatedRequest("POST", c.endpoints.Tenants, - withPayload(tenant, CONTENT_TYPE_TENANT), - )) +func (c *MeshTenantClient) Create(tenant *MeshTenantCreate) (*MeshTenant, error) { + return c.post(tenant) } -func (c *MeshStackProviderClient) DeleteTenant(workspace string, project string, platform string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenant(workspace, project, platform), - withAccept(CONTENT_TYPE_TENANT), - ) - return err +func (c *MeshTenantClient) Delete(workspace string, project string, platform string) error { + return c.delete(c.tenantId(workspace, project, platform)) } diff --git a/tenant_v4.go b/tenant_v4.go index 6243a95..226ba41 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -3,14 +3,11 @@ package client import ( "context" "fmt" - "net/url" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" ) -const CONTENT_TYPE_TENANT_V4 = "application/vnd.meshcloud.api.meshtenant.v4-preview.hal+json" - type MeshTenantV4 struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -59,42 +56,35 @@ type MeshTenantV4CreateSpec struct { Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } -func (c *MeshStackProviderClient) urlForTenantV4(uuid string) *url.URL { - return c.endpoints.Tenants.JoinPath(uuid) +type MeshTenantV4Client struct { + meshObjectClient[MeshTenantV4] } -func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, error) { - return unmarshalBodyIfPresent[MeshTenantV4](c.doAuthenticatedRequest("GET", c.urlForTenantV4(uuid), - withAccept(CONTENT_TYPE_TENANT_V4), - )) +func (c MeshTenantV4Client) Read(uuid string) (*MeshTenantV4, error) { + return c.get(uuid) } -func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { - return unmarshalBody[MeshTenantV4](c.doAuthenticatedRequest("POST", c.endpoints.Tenants, - withPayload(tenant, CONTENT_TYPE_TENANT_V4), - )) +func (c MeshTenantV4Client) Create(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { + return c.post(tenant) } -func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForTenantV4(uuid), - withAccept(CONTENT_TYPE_TENANT_V4), - ) - return err +func (c MeshTenantV4Client) Delete(uuid string) error { + return c.delete(uuid) } -// PollTenantV4UntilCreation polls a tenant until creation completes (platformTenantId is set) +// PollUntilCreation polls a tenant until creation completes (platformTenantId is set) // Returns the final tenant state or an error if polling fails or times out. -func (c *MeshStackProviderClient) PollTenantV4UntilCreation(ctx context.Context, uuid string) (*MeshTenantV4, error) { +func (c MeshTenantV4Client) PollUntilCreation(ctx context.Context, uuid string) (*MeshTenantV4, error) { var result *MeshTenantV4 - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForTenantV4CreationFunc(uuid, &result)) + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCreationFunc(uuid, &result)) return result, err } -// waitForTenantV4CreationFunc returns a RetryFunc that checks tenant creation status. -func (c *MeshStackProviderClient) waitForTenantV4CreationFunc(uuid string, result **MeshTenantV4) retry.RetryFunc { +// waitForCreationFunc returns a RetryFunc that checks tenant creation status. +func (c MeshTenantV4Client) waitForCreationFunc(uuid string, result **MeshTenantV4) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.ReadTenantV4(uuid) + current, err := c.Read(uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for creation: %w", err)) } @@ -114,16 +104,16 @@ func (c *MeshStackProviderClient) waitForTenantV4CreationFunc(uuid string, resul } } -// PollTenantV4UntilDeletion polls a tenant until it is deleted (not found) +// PollUntilDeletion polls a tenant until it is deleted (not found) // Returns nil on successful deletion or an error if polling fails or times out. -func (c *MeshStackProviderClient) PollTenantV4UntilDeletion(ctx context.Context, uuid string) error { - return retry.RetryContext(ctx, 30*time.Minute, c.waitForTenantV4DeletionFunc(uuid)) +func (c MeshTenantV4Client) PollUntilDeletion(ctx context.Context, uuid string) error { + return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(uuid)) } -// waitForTenantV4DeletionFunc returns a RetryFunc that checks tenant deletion status. -func (c *MeshStackProviderClient) waitForTenantV4DeletionFunc(uuid string) retry.RetryFunc { +// waitForDeletionFunc returns a RetryFunc that checks tenant deletion status. +func (c MeshTenantV4Client) waitForDeletionFunc(uuid string) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.ReadTenantV4(uuid) + current, err := c.Read(uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for deletion: %w", err)) } diff --git a/workspace.go b/workspace.go index 309ec38..695895e 100644 --- a/workspace.go +++ b/workspace.go @@ -1,11 +1,5 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_WORKSPACE = "application/vnd.meshcloud.api.meshworkspace.v2.hal+json" - type MeshWorkspace struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -35,31 +29,22 @@ type MeshWorkspaceCreateMetadata struct { Tags map[string][]string `json:"tags" tfsdk:"tags"` } -func (c *MeshStackProviderClient) urlForWorkspace(name string) *url.URL { - return c.endpoints.Workspaces.JoinPath(name) +type MeshWorkspaceClient struct { + meshObjectClient[MeshWorkspace] } -func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, error) { - return unmarshalBodyIfPresent[MeshWorkspace](c.doAuthenticatedRequest("GET", c.urlForWorkspace(name), - withAccept(CONTENT_TYPE_WORKSPACE), - )) +func (c MeshWorkspaceClient) Read(name string) (*MeshWorkspace, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest("POST", c.endpoints.Workspaces, - withPayload(workspace, CONTENT_TYPE_WORKSPACE), - )) +func (c MeshWorkspaceClient) Create(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + return c.post(workspace) } -func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - return unmarshalBody[MeshWorkspace](c.doAuthenticatedRequest("PUT", c.urlForWorkspace(name), - withPayload(workspace, CONTENT_TYPE_WORKSPACE), - )) +func (c MeshWorkspaceClient) Update(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + return c.put(name, workspace) } -func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspace(name), - withAccept(CONTENT_TYPE_WORKSPACE), - ) - return err +func (c MeshWorkspaceClient) Delete(name string) error { + return c.delete(name) } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 2fddb5c..787edba 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -1,32 +1,19 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_WORKSPACE_GROUP_BINDING = "application/vnd.meshcloud.api.meshworkspacegroupbinding.v2.hal+json" - type MeshWorkspaceGroupBinding = MeshWorkspaceBinding -func (c *MeshStackProviderClient) urlForWorkspaceGroupBinding(name string) *url.URL { - return c.endpoints.WorkspaceGroupBindings.JoinPath(name) +type MeshWorkspaceGroupBindingClient struct { + meshObjectClient[MeshWorkspaceBinding] } -func (c *MeshStackProviderClient) ReadWorkspaceGroupBinding(name string) (*MeshWorkspaceGroupBinding, error) { - return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest("GET", c.urlForWorkspaceGroupBinding(name), - withAccept(CONTENT_TYPE_WORKSPACE_GROUP_BINDING), - )) +func (c MeshWorkspaceGroupBindingClient) Read(name string) (*MeshWorkspaceGroupBinding, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateWorkspaceGroupBinding(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { - return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest("POST", c.endpoints.WorkspaceGroupBindings, - withPayload(binding, CONTENT_TYPE_WORKSPACE_GROUP_BINDING), - )) +func (c MeshWorkspaceGroupBindingClient) Create(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { + return c.post(binding) } -func (c *MeshStackProviderClient) DeleteWorkspaceGroupBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceGroupBinding(name), - withAccept(CONTENT_TYPE_WORKSPACE_GROUP_BINDING), - ) - return err +func (c MeshWorkspaceGroupBindingClient) Delete(name string) error { + return c.delete(name) } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index c10d77c..0ae752a 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -1,32 +1,19 @@ package client -import ( - "net/url" -) - -const CONTENT_TYPE_WORKSPACE_USER_BINDING = "application/vnd.meshcloud.api.meshworkspaceuserbinding.v2.hal+json" - type MeshWorkspaceUserBinding = MeshWorkspaceBinding -func (c *MeshStackProviderClient) urlForWorkspaceUserBinding(name string) *url.URL { - return c.endpoints.WorkspaceUserBindings.JoinPath(name) +type MeshWorkspaceUserBindingClient struct { + meshObjectClient[MeshWorkspaceBinding] } -func (c *MeshStackProviderClient) ReadWorkspaceUserBinding(name string) (*MeshWorkspaceUserBinding, error) { - return unmarshalBodyIfPresent[MeshWorkspaceBinding](c.doAuthenticatedRequest("GET", c.urlForWorkspaceUserBinding(name), - withAccept(CONTENT_TYPE_WORKSPACE_USER_BINDING), - )) +func (c MeshWorkspaceUserBindingClient) Read(name string) (*MeshWorkspaceUserBinding, error) { + return c.get(name) } -func (c *MeshStackProviderClient) CreateWorkspaceUserBinding(binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { - return unmarshalBody[MeshWorkspaceBinding](c.doAuthenticatedRequest("POST", c.endpoints.WorkspaceUserBindings, - withPayload(binding, CONTENT_TYPE_WORKSPACE_USER_BINDING), - )) +func (c MeshWorkspaceUserBindingClient) Create(binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { + return c.post(binding) } -func (c *MeshStackProviderClient) DeleteWorkspaceUserBinding(name string) error { - _, err := c.doAuthenticatedRequest("DELETE", c.urlForWorkspaceUserBinding(name), - withAccept(CONTENT_TYPE_WORKSPACE_USER_BINDING), - ) - return err +func (c MeshWorkspaceUserBindingClient) Delete(name string) error { + return c.delete(name) } From bff3a79d71210a9b09834fb2cdb2c6e49db58b2e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 22:42:57 +0100 Subject: [PATCH 073/200] refactor: simplify httpClient methods and url query handling --- client.go | 193 ++++++++++++++++++++++++----------------------------- project.go | 12 ++-- tenant.go | 8 +-- 3 files changed, 97 insertions(+), 116 deletions(-) diff --git a/client.go b/client.go index 84cd423..88cf054 100644 --- a/client.go +++ b/client.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "iter" "log" "net/http" "net/url" @@ -97,31 +96,66 @@ func pluralizeName(name string) string { return fmt.Sprintf("%ss", name) } -func (o meshObjectClient[M]) mediaType() string { - return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", o.Name, o.ApiVersion) +func (c meshObjectClient[M]) meshObjectMimeType() string { + return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", c.Name, c.ApiVersion) } func (c meshObjectClient[M]) get(id string) (*M, error) { - return unmarshalBodyIfPresent[M](c.doAuthenticatedRequest(http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.mediaType()))) -} - -func (c meshObjectClient[M]) list(options ...doRequestOption) ([]M, error) { - return unmarshalBodyPages[M](pluralizeName(c.Name), c.doPaginatedRequest(c.ApiUrl, append(options, withAccept(c.mediaType()))...)) + body, err := c.doAuthenticatedRequest(http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) + if errors.Is(err, errNotFound) { + return nil, nil + } + return unmarshalBody[M](body, err) } func (c meshObjectClient[M]) post(payload any) (*M, error) { - return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPost, c.ApiUrl, withPayload(payload, c.mediaType()))) + return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPost, c.ApiUrl, withPayload(payload, c.meshObjectMimeType()))) } func (c meshObjectClient[M]) put(id string, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.mediaType()))) + return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.meshObjectMimeType()))) } func (c meshObjectClient[M]) delete(id string) (err error) { - _, err = c.doAuthenticatedRequest(http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.mediaType())) + _, err = c.doAuthenticatedRequest(http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) return } +func (c meshObjectClient[M]) list(options ...doRequestOption) ([]M, error) { + var result []M + embeddedKey := pluralizeName(c.Name) + pageNumber := 0 + for { + body, err := c.doAuthenticatedRequest(http.MethodGet, c.ApiUrl, append(options, + withAccept(c.meshObjectMimeType()), + withUrlQuery("page", pageNumber), + )...) + if err != nil { + return result, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err) + } + type paginatedResponse struct { + Embedded map[string][]M `json:"_embedded"` + Page struct { + TotalPages int `json:"totalPages"` + Number int `json:"number"` + } `json:"page"` + } + response, err := unmarshalBody[paginatedResponse](body, err) + if err != nil { + return result, fmt.Errorf("cannot unmarshal paginated response, page %d: %w", pageNumber, err) + } else if items, ok := response.Embedded[embeddedKey]; !ok { + return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) + } else { + result = append(result, items...) + } + // check if we've reached the end of pagination + if response.Page.Number >= response.Page.TotalPages-1 { + return result, nil + } + pageNumber++ + } +} + func (c *httpClient) login() error { loginApiUrl := c.RootUrl.JoinPath("/api/login") @@ -156,22 +190,14 @@ func (c *httpClient) ensureValidToken() error { type doRequestOption func(opts *doRequestOptions) -type urlModifier func(url *url.URL) - type requestModifier func(req *http.Request) type doRequestOptions struct { - urlModifiers []urlModifier + urlQueryParams map[string]string requestPayload any requestModifiers []requestModifier } -func appendUrlModifier(modifier urlModifier) doRequestOption { - return func(opts *doRequestOptions) { - opts.urlModifiers = append(opts.urlModifiers, modifier) - } -} - func appendRequestModifier(modifier requestModifier) doRequestOption { return func(opts *doRequestOptions) { opts.requestModifiers = append(opts.requestModifiers, modifier) @@ -179,17 +205,19 @@ func appendRequestModifier(modifier requestModifier) doRequestOption { } func withUrlQuery(key string, value any) doRequestOption { - return appendUrlModifier(func(url *url.URL) { + return func(opts *doRequestOptions) { var valueStr string if stringerValue, ok := value.(fmt.Stringer); ok { valueStr = stringerValue.String() } else { valueStr = fmt.Sprintf("%v", value) } - query := url.Query() - query.Set(key, valueStr) - url.RawQuery = query.Encode() - }) + if opts.urlQueryParams == nil { + opts.urlQueryParams = map[string]string{key: valueStr} + } else { + opts.urlQueryParams[key] = valueStr + } + } } func withAccept(accept string) doRequestOption { @@ -222,32 +250,10 @@ func (c *httpClient) doRequest(method string, url *url.URL, options ...doRequest for _, option := range options { option(&opts) } - - if len(opts.urlModifiers) > 0 { - // clone url to prevent modifiers edit the given URL (it's sad that this is a pointer actually) - // ignoring the error is fine as this always succeeds parsing from String() - url, _ = url.Parse(url.String()) - for _, modifier := range opts.urlModifiers { - modifier(url) - } - } - - var requestBody io.ReadWriter - if opts.requestPayload != nil { - requestBody = new(bytes.Buffer) - if err := json.NewEncoder(requestBody).Encode(opts.requestPayload); err != nil { - return nil, fmt.Errorf("failed to encode request body payload: %w", err) - } - } - - req, err := http.NewRequest(method, url.String(), requestBody) + req, err := c.buildRequest(method, *url, opts) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - for _, requestModifier := range opts.requestModifiers { - requestModifier(req) + return nil, err } - res, err := c.Do(req) if err != nil { return nil, err @@ -256,7 +262,10 @@ func (c *httpClient) doRequest(method string, url *url.URL, options ...doRequest _ = res.Body.Close() }() log.Println(res) + return c.readBodyAndCheckSuccess(res) +} +func (c *httpClient) readBodyAndCheckSuccess(res *http.Response) ([]byte, error) { responseBody, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) @@ -279,6 +288,35 @@ func (c *httpClient) doRequest(method string, url *url.URL, options ...doRequest return responseBody, errors.Join(errs...) } +func (c *httpClient) buildRequest(method string, url url.URL, opts doRequestOptions) (*http.Request, error) { + if len(opts.urlQueryParams) > 0 { + query := url.Query() + for k, v := range opts.urlQueryParams { + query.Set(k, v) + } + // Note: url is not a pointer here, + // so we can safely update that struct field without propagating such change to the caller! + url.RawQuery = query.Encode() + } + + var requestBody io.ReadWriter + if opts.requestPayload != nil { + requestBody = new(bytes.Buffer) + if err := json.NewEncoder(requestBody).Encode(opts.requestPayload); err != nil { + return nil, fmt.Errorf("failed to encode request body payload: %w", err) + } + } + + req, err := http.NewRequest(method, url.String(), requestBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + for _, requestModifier := range opts.requestModifiers { + requestModifier(req) + } + return req, err +} + func (c *httpClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { if err := c.ensureValidToken(); err != nil { return nil, err @@ -292,39 +330,6 @@ func (c *httpClient) doAuthenticatedRequest(method string, url *url.URL, options )...) } -func (c *httpClient) doPaginatedRequest(url *url.URL, options ...doRequestOption) iter.Seq2[[]byte, error] { - return func(yield func([]byte, error) bool) { - pageNumber := 0 - for { - body, err := c.doAuthenticatedRequest(http.MethodGet, url, append(options, withUrlQuery("page", pageNumber))...) - if err != nil { - yield(body, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err)) - return - } - if !yield(body, nil) { - // consumer wants to stop - return - } - // Check if there are more pages to fetch - type paginatedResponse struct { - Page struct { - TotalPages int `json:"totalPages"` - Number int `json:"number"` - } `json:"page"` - } - response, err := unmarshalBody[paginatedResponse](body, err) - if err != nil { - yield(body, fmt.Errorf("cannot unmarshal paginated response, page %d: %w", pageNumber, err)) - return - } - if response.Page.Number >= response.Page.TotalPages-1 { - return - } - pageNumber++ - } - } -} - func unmarshalBody[T any](body []byte, err error) (*T, error) { if err != nil { return nil, err @@ -335,27 +340,3 @@ func unmarshalBody[T any](body []byte, err error) (*T, error) { } return &target, nil } - -func unmarshalBodyIfPresent[T any](body []byte, err error) (*T, error) { - if errors.Is(err, errNotFound) { - return nil, nil - } - return unmarshalBody[T](body, err) -} - -func unmarshalBodyPages[T any](embeddedKey string, bodyPages iter.Seq2[[]byte, error]) ([]T, error) { - var result []T - for bodyPage, pageErr := range bodyPages { - type embeddedResponse[T any] struct { - Embedded map[string][]T `json:"_embedded"` - } - if response, err := unmarshalBody[embeddedResponse[T]](bodyPage, pageErr); err != nil { - return result, err - } else if items, ok := response.Embedded[embeddedKey]; !ok { - return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) - } else { - result = append(result, items...) - } - } - return result, nil -} diff --git a/project.go b/project.go index cb509b2..5aca292 100644 --- a/project.go +++ b/project.go @@ -35,15 +35,15 @@ type MeshProjectClient struct { meshObjectClient[MeshProject] } -func (c *MeshProjectClient) projectId(workspace string, name string) string { +func (c MeshProjectClient) projectId(workspace string, name string) string { return workspace + "." + name } -func (c *MeshProjectClient) Read(workspace string, name string) (*MeshProject, error) { +func (c MeshProjectClient) Read(workspace string, name string) (*MeshProject, error) { return c.get(c.projectId(workspace, name)) } -func (c *MeshProjectClient) List(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { +func (c MeshProjectClient) List(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { options := []doRequestOption{ withUrlQuery("workspaceIdentifier", workspaceIdentifier), } @@ -53,14 +53,14 @@ func (c *MeshProjectClient) List(workspaceIdentifier string, paymentMethodIdenti return c.list(options...) } -func (c *MeshProjectClient) Create(project *MeshProjectCreate) (*MeshProject, error) { +func (c MeshProjectClient) Create(project *MeshProjectCreate) (*MeshProject, error) { return c.post(project) } -func (c *MeshProjectClient) Update(project *MeshProjectCreate) (*MeshProject, error) { +func (c MeshProjectClient) Update(project *MeshProjectCreate) (*MeshProject, error) { return c.put(c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) } -func (c *MeshProjectClient) Delete(workspace string, name string) error { +func (c MeshProjectClient) Delete(workspace string, name string) error { return c.delete(c.projectId(workspace, name)) } diff --git a/tenant.go b/tenant.go index d1b5a32..6acaa5f 100644 --- a/tenant.go +++ b/tenant.go @@ -47,18 +47,18 @@ type MeshTenantClient struct { meshObjectClient[MeshTenant] } -func (c *MeshTenantClient) tenantId(workspace string, project string, platform string) string { +func (c MeshTenantClient) tenantId(workspace string, project string, platform string) string { return workspace + "." + project + "." + platform } -func (c *MeshTenantClient) Read(workspace string, project string, platform string) (*MeshTenant, error) { +func (c MeshTenantClient) Read(workspace string, project string, platform string) (*MeshTenant, error) { return c.get(c.tenantId(workspace, project, platform)) } -func (c *MeshTenantClient) Create(tenant *MeshTenantCreate) (*MeshTenant, error) { +func (c MeshTenantClient) Create(tenant *MeshTenantCreate) (*MeshTenant, error) { return c.post(tenant) } -func (c *MeshTenantClient) Delete(workspace string, project string, platform string) error { +func (c MeshTenantClient) Delete(workspace string, project string, platform string) error { return c.delete(c.tenantId(workspace, project, platform)) } From 3c6c8473829530c13a96f514a524fc5b1d32705b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 22:48:31 +0100 Subject: [PATCH 074/200] feat: change user agent such that the provider version is included --- client.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 88cf054..d15a0cf 100644 --- a/client.go +++ b/client.go @@ -37,13 +37,14 @@ type MeshStackProviderClient struct { WorkspaceUserBinding MeshWorkspaceUserBindingClient } -func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) MeshStackProviderClient { +func NewClient(rootUrl *url.URL, providerVersion, apiKey, apiSecret string) MeshStackProviderClient { // Initialize httpClient for typed clients c := &httpClient{ - Client: http.Client{Timeout: 5 * time.Minute}, - RootUrl: rootUrl, - ApiKey: apiKey, - ApiSecret: apiSecret, + Client: http.Client{Timeout: 5 * time.Minute}, + RootUrl: rootUrl, + ProviderVersion: providerVersion, + ApiKey: apiKey, + ApiSecret: apiSecret, } return MeshStackProviderClient{ MeshBuildingBlockClient{newMeshObjectClient[MeshBuildingBlock](c, "meshBuildingBlock", "v1")}, @@ -67,7 +68,9 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) MeshStackProvi type httpClient struct { http.Client - RootUrl *url.URL + RootUrl *url.URL + ProviderVersion string + ApiKey string ApiSecret string Token string @@ -244,7 +247,7 @@ func withPayload(payload any, contentType string) doRequestOption { func (c *httpClient) doRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { // prepend (aka insert at 0) some default options such that given options may be overridden by caller options = slices.Insert(options, 0, - withHeader("User-Agent", "meshStack Terraform Provider"), + withHeader("User-Agent", fmt.Sprintf("terraform-provider-meshstack/%s", c.ProviderVersion)), ) opts := doRequestOptions{} for _, option := range options { From 788b5d192bf88c849eb2be123782297243ad0865 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 9 Jan 2026 23:54:38 +0100 Subject: [PATCH 075/200] refactor: clean up client.NewClient() with factory methods and reflection --- buildingblock.go | 4 +++ buildingblock_v2.go | 4 +++ client.go | 55 ++++++++++++++++++++---------- client_test.go | 68 ++++++++++++++++++++++++++++++++++++++ integrations.go | 4 +++ landingzone.go | 4 +++ location.go | 4 +++ payment_method.go | 4 +++ platform.go | 4 +++ project.go | 4 +++ project_group_binding.go | 10 ++++-- project_user_binding.go | 10 ++++-- tag_definition.go | 4 +++ tenant.go | 4 +++ tenant_v4.go | 4 +++ workspace.go | 4 +++ workspace_group_binding.go | 10 ++++-- workspace_user_binding.go | 10 ++++-- 18 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 client_test.go diff --git a/buildingblock.go b/buildingblock.go index 19f4f44..3798b34 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -74,6 +74,10 @@ type MeshBuildingBlockClient struct { meshObjectClient[MeshBuildingBlock] } +func newBuildingBlockClient(c *httpClient) MeshBuildingBlockClient { + return MeshBuildingBlockClient{newMeshObjectClient[MeshBuildingBlock](c, "v1")} +} + func (c MeshBuildingBlockClient) Read(uuid string) (*MeshBuildingBlock, error) { return c.get(uuid) } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index b767b76..b69cf73 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -69,6 +69,10 @@ type MeshBuildingBlockV2Client struct { meshObjectClient[MeshBuildingBlockV2] } +func newBuildingBlockV2Client(c *httpClient) MeshBuildingBlockV2Client { + return MeshBuildingBlockV2Client{newMeshObjectClient[MeshBuildingBlockV2](c, "v2-preview")} +} + func (c MeshBuildingBlockV2Client) Read(uuid string) (*MeshBuildingBlockV2, error) { return c.get(uuid) } diff --git a/client.go b/client.go index d15a0cf..7b938c5 100644 --- a/client.go +++ b/client.go @@ -9,9 +9,11 @@ import ( "log" "net/http" "net/url" + "reflect" "slices" "strings" "time" + "unicode" ) var ( @@ -47,22 +49,22 @@ func NewClient(rootUrl *url.URL, providerVersion, apiKey, apiSecret string) Mesh ApiSecret: apiSecret, } return MeshStackProviderClient{ - MeshBuildingBlockClient{newMeshObjectClient[MeshBuildingBlock](c, "meshBuildingBlock", "v1")}, - MeshBuildingBlockV2Client{newMeshObjectClient[MeshBuildingBlockV2](c, "meshBuildingBlock", "v2-preview")}, - MeshIntegrationClient{newMeshObjectClient[MeshIntegration](c, "meshIntegration", "v1-preview")}, - MeshLandingZoneClient{newMeshObjectClient[MeshLandingZone](c, "meshLandingZone", "v1-preview")}, - MeshLocationClient{newMeshObjectClient[MeshLocation](c, "meshLocation", "v1-preview")}, - MeshPaymentMethodClient{newMeshObjectClient[MeshPaymentMethod](c, "meshPaymentMethod", "v2")}, - MeshPlatformClient{newMeshObjectClient[MeshPlatform](c, "meshPlatform", "v2-preview")}, - MeshProjectClient{newMeshObjectClient[MeshProject](c, "meshProject", "v2")}, - MeshProjectGroupBindingClient{newMeshObjectClient[MeshProjectBinding](c, "meshProjectGroupBinding", "v3", "meshprojectbindings", "groupbindings")}, - MeshProjectUserBindingClient{newMeshObjectClient[MeshProjectBinding](c, "meshProjectUserBinding", "v3", "meshprojectbindings", "userbindings")}, - MeshTagDefinitionClient{newMeshObjectClient[MeshTagDefinition](c, "meshTagDefinition", "v1")}, - MeshTenantClient{newMeshObjectClient[MeshTenant](c, "meshTenant", "v3")}, - MeshTenantV4Client{newMeshObjectClient[MeshTenantV4](c, "meshTenant", "v4-preview")}, - MeshWorkspaceClient{newMeshObjectClient[MeshWorkspace](c, "meshWorkspace", "v2")}, - MeshWorkspaceGroupBindingClient{newMeshObjectClient[MeshWorkspaceBinding](c, "meshWorkspaceGroupBinding", "v2", "meshworkspacebindings", "groupbindings")}, - MeshWorkspaceUserBindingClient{newMeshObjectClient[MeshWorkspaceBinding](c, "meshWorkspaceUserBinding", "v2", "meshworkspacebindings", "userbindings")}, + newBuildingBlockClient(c), + newBuildingBlockV2Client(c), + newIntegrationClient(c), + newLandingZoneClient(c), + newLocationClient(c), + newPaymentMethodClient(c), + newPlatformClient(c), + newProjectClient(c), + newProjectGroupBindingClient(c), + newProjectUserBindingClient(c), + newTagDefinitionClient(c), + newTenantClient(c), + newTenantV4Client(c), + newWorkspaceClient(c), + newWorkspaceGroupBindingClient(c), + newWorkspaceUserBindingClient(c), } } @@ -83,7 +85,9 @@ type meshObjectClient[M any] struct { ApiUrl *url.URL } -func newMeshObjectClient[M any](client *httpClient, name, apiVersion string, explicitApiPaths ...string) meshObjectClient[M] { +func newMeshObjectClient[M any](client *httpClient, apiVersion string, explicitApiPaths ...string) meshObjectClient[M] { + name := inferMeshObjectName[M]() + if len(explicitApiPaths) == 0 { // infer API path from meshObject name by default (if nothing explicit is given) explicitApiPaths = []string{strings.ToLower(pluralizeName(name))} @@ -95,6 +99,23 @@ func newMeshObjectClient[M any](client *httpClient, name, apiVersion string, exp return meshObjectClient[M]{client, name, apiVersion, apiUrl} } +// inferMeshObjectName uses reflection to infer the meshObject name from the type parameter M. +// It converts the type name to camelCase (e.g., "MeshBuildingBlock" -> "meshBuildingBlock"). +func inferMeshObjectName[M any]() string { + var zero M + typeName := reflect.TypeOf(zero).Name() + return lowercaseFirst(typeName) +} + +func lowercaseFirst(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToLower(runes[0]) + return string(runes) +} + func pluralizeName(name string) string { return fmt.Sprintf("%ss", name) } diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..8b5f6f3 --- /dev/null +++ b/client_test.go @@ -0,0 +1,68 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInferMeshObjectName(t *testing.T) { + tests := []struct { + name string + testFunc func() string + expected string + }{ + { + name: "MeshBuildingBlock", + testFunc: inferMeshObjectName[MeshBuildingBlock], + expected: "meshBuildingBlock", + }, + { + name: "MeshBuildingBlockV2", + testFunc: inferMeshObjectName[MeshBuildingBlockV2], + expected: "meshBuildingBlockV2", + }, + { + name: "MeshProject", + testFunc: inferMeshObjectName[MeshProject], + expected: "meshProject", + }, + { + name: "MeshWorkspace", + testFunc: inferMeshObjectName[MeshWorkspace], + expected: "meshWorkspace", + }, + { + name: "MeshProjectBinding", + testFunc: inferMeshObjectName[MeshProjectBinding], + expected: "meshProjectBinding", + }, + { + name: "MeshProjectGroupBinding (embedded struct)", + testFunc: inferMeshObjectName[MeshProjectGroupBinding], + expected: "meshProjectGroupBinding", + }, + { + name: "MeshProjectUserBinding (embedded struct)", + testFunc: inferMeshObjectName[MeshProjectUserBinding], + expected: "meshProjectUserBinding", + }, + { + name: "MeshWorkspaceGroupBinding (embedded struct)", + testFunc: inferMeshObjectName[MeshWorkspaceGroupBinding], + expected: "meshWorkspaceGroupBinding", + }, + { + name: "MeshWorkspaceUserBinding (embedded struct)", + testFunc: inferMeshObjectName[MeshWorkspaceUserBinding], + expected: "meshWorkspaceUserBinding", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.testFunc() + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/integrations.go b/integrations.go index 9aa514a..d9d192d 100644 --- a/integrations.go +++ b/integrations.go @@ -83,6 +83,10 @@ type MeshIntegrationClient struct { meshObjectClient[MeshIntegration] } +func newIntegrationClient(c *httpClient) MeshIntegrationClient { + return MeshIntegrationClient{newMeshObjectClient[MeshIntegration](c, "v1-preview")} +} + func (c MeshIntegrationClient) integrationId(workspace string, uuid string) string { return workspace + "/" + uuid } diff --git a/landingzone.go b/landingzone.go index 0967a8c..9521d84 100644 --- a/landingzone.go +++ b/landingzone.go @@ -63,6 +63,10 @@ type MeshLandingZoneClient struct { meshObjectClient[MeshLandingZone] } +func newLandingZoneClient(c *httpClient) MeshLandingZoneClient { + return MeshLandingZoneClient{newMeshObjectClient[MeshLandingZone](c, "v1-preview")} +} + func (c MeshLandingZoneClient) Read(name string) (*MeshLandingZone, error) { return c.get(name) } diff --git a/location.go b/location.go index 8182416..3e8ced9 100644 --- a/location.go +++ b/location.go @@ -35,6 +35,10 @@ type MeshLocationClient struct { meshObjectClient[MeshLocation] } +func newLocationClient(c *httpClient) MeshLocationClient { + return MeshLocationClient{newMeshObjectClient[MeshLocation](c, "v1-preview")} +} + func (c MeshLocationClient) Read(name string) (*MeshLocation, error) { return c.get(name) } diff --git a/payment_method.go b/payment_method.go index 84f73f2..6ce5378 100644 --- a/payment_method.go +++ b/payment_method.go @@ -36,6 +36,10 @@ type MeshPaymentMethodClient struct { meshObjectClient[MeshPaymentMethod] } +func newPaymentMethodClient(c *httpClient) MeshPaymentMethodClient { + return MeshPaymentMethodClient{newMeshObjectClient[MeshPaymentMethod](c, "v2")} +} + func (c MeshPaymentMethodClient) Read(workspace string, identifier string) (*MeshPaymentMethod, error) { return c.get(identifier) } diff --git a/platform.go b/platform.go index cc32525..7d6a88f 100644 --- a/platform.go +++ b/platform.go @@ -107,6 +107,10 @@ type MeshPlatformClient struct { meshObjectClient[MeshPlatform] } +func newPlatformClient(c *httpClient) MeshPlatformClient { + return MeshPlatformClient{newMeshObjectClient[MeshPlatform](c, "v2-preview")} +} + func (c MeshPlatformClient) Read(uuid string) (*MeshPlatform, error) { return c.get(uuid) } diff --git a/project.go b/project.go index 5aca292..d4b5f7b 100644 --- a/project.go +++ b/project.go @@ -35,6 +35,10 @@ type MeshProjectClient struct { meshObjectClient[MeshProject] } +func newProjectClient(c *httpClient) MeshProjectClient { + return MeshProjectClient{newMeshObjectClient[MeshProject](c, "v2")} +} + func (c MeshProjectClient) projectId(workspace string, name string) string { return workspace + "." + name } diff --git a/project_group_binding.go b/project_group_binding.go index 96d6640..d3e7764 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -1,9 +1,15 @@ package client -type MeshProjectGroupBinding = MeshProjectBinding +type MeshProjectGroupBinding struct { + MeshProjectBinding +} type MeshProjectGroupBindingClient struct { - meshObjectClient[MeshProjectBinding] + meshObjectClient[MeshProjectGroupBinding] +} + +func newProjectGroupBindingClient(c *httpClient) MeshProjectGroupBindingClient { + return MeshProjectGroupBindingClient{newMeshObjectClient[MeshProjectGroupBinding](c, "v3", "meshprojectbindings", "groupbindings")} } func (c MeshProjectGroupBindingClient) Read(name string) (*MeshProjectGroupBinding, error) { diff --git a/project_user_binding.go b/project_user_binding.go index ee619d3..51df0ba 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -1,9 +1,15 @@ package client -type MeshProjectUserBinding = MeshProjectBinding +type MeshProjectUserBinding struct { + MeshProjectBinding +} type MeshProjectUserBindingClient struct { - meshObjectClient[MeshProjectBinding] + meshObjectClient[MeshProjectUserBinding] +} + +func newProjectUserBindingClient(c *httpClient) MeshProjectUserBindingClient { + return MeshProjectUserBindingClient{newMeshObjectClient[MeshProjectUserBinding](c, "v3", "meshprojectbindings", "userbindings")} } func (c MeshProjectUserBindingClient) Read(name string) (*MeshProjectUserBinding, error) { diff --git a/tag_definition.go b/tag_definition.go index 884b324..dfba7a2 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -67,6 +67,10 @@ type MeshTagDefinitionClient struct { meshObjectClient[MeshTagDefinition] } +func newTagDefinitionClient(c *httpClient) MeshTagDefinitionClient { + return MeshTagDefinitionClient{newMeshObjectClient[MeshTagDefinition](c, "v1")} +} + func (c MeshTagDefinitionClient) List() ([]MeshTagDefinition, error) { return c.list() } diff --git a/tenant.go b/tenant.go index 6acaa5f..7357113 100644 --- a/tenant.go +++ b/tenant.go @@ -47,6 +47,10 @@ type MeshTenantClient struct { meshObjectClient[MeshTenant] } +func newTenantClient(c *httpClient) MeshTenantClient { + return MeshTenantClient{newMeshObjectClient[MeshTenant](c, "v3")} +} + func (c MeshTenantClient) tenantId(workspace string, project string, platform string) string { return workspace + "." + project + "." + platform } diff --git a/tenant_v4.go b/tenant_v4.go index 226ba41..6ba3f0c 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -60,6 +60,10 @@ type MeshTenantV4Client struct { meshObjectClient[MeshTenantV4] } +func newTenantV4Client(c *httpClient) MeshTenantV4Client { + return MeshTenantV4Client{newMeshObjectClient[MeshTenantV4](c, "v4-preview")} +} + func (c MeshTenantV4Client) Read(uuid string) (*MeshTenantV4, error) { return c.get(uuid) } diff --git a/workspace.go b/workspace.go index 695895e..56f0f65 100644 --- a/workspace.go +++ b/workspace.go @@ -33,6 +33,10 @@ type MeshWorkspaceClient struct { meshObjectClient[MeshWorkspace] } +func newWorkspaceClient(c *httpClient) MeshWorkspaceClient { + return MeshWorkspaceClient{newMeshObjectClient[MeshWorkspace](c, "v2")} +} + func (c MeshWorkspaceClient) Read(name string) (*MeshWorkspace, error) { return c.get(name) } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 787edba..5d718f0 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -1,9 +1,15 @@ package client -type MeshWorkspaceGroupBinding = MeshWorkspaceBinding +type MeshWorkspaceGroupBinding struct { + MeshWorkspaceBinding +} type MeshWorkspaceGroupBindingClient struct { - meshObjectClient[MeshWorkspaceBinding] + meshObjectClient[MeshWorkspaceGroupBinding] +} + +func newWorkspaceGroupBindingClient(c *httpClient) MeshWorkspaceGroupBindingClient { + return MeshWorkspaceGroupBindingClient{newMeshObjectClient[MeshWorkspaceGroupBinding](c, "v2", "meshworkspacebindings", "groupbindings")} } func (c MeshWorkspaceGroupBindingClient) Read(name string) (*MeshWorkspaceGroupBinding, error) { diff --git a/workspace_user_binding.go b/workspace_user_binding.go index 0ae752a..c9c94ec 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -1,9 +1,15 @@ package client -type MeshWorkspaceUserBinding = MeshWorkspaceBinding +type MeshWorkspaceUserBinding struct { + MeshWorkspaceBinding +} type MeshWorkspaceUserBindingClient struct { - meshObjectClient[MeshWorkspaceBinding] + meshObjectClient[MeshWorkspaceUserBinding] +} + +func newWorkspaceUserBindingClient(c *httpClient) MeshWorkspaceUserBindingClient { + return MeshWorkspaceUserBindingClient{newMeshObjectClient[MeshWorkspaceUserBinding](c, "v2", "meshworkspacebindings", "userbindings")} } func (c MeshWorkspaceUserBindingClient) Read(name string) (*MeshWorkspaceUserBinding, error) { From 69482caf79d19f4ec90dfe603b05b00aab15a5e6 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Sat, 10 Jan 2026 13:06:39 +0100 Subject: [PATCH 076/200] refactor: move MeshObjectClient into client/internal --- buildingblock.go | 18 +- buildingblock_v2.go | 18 +- client.go | 358 ++---------------- integrations.go | 16 +- internal/http_client.go | 112 ++++++ internal/mesh_object_client.go | 170 +++++++++ .../mesh_object_client_test.go | 16 +- internal/options.go | 59 +++ landingzone.go | 20 +- location.go | 20 +- payment_method.go | 20 +- platform.go | 20 +- project.go | 28 +- project_group_binding.go | 18 +- project_user_binding.go | 18 +- tag_definition.go | 22 +- tenant.go | 18 +- tenant_v4.go | 21 +- workspace.go | 20 +- workspace_group_binding.go | 18 +- workspace_user_binding.go | 18 +- 21 files changed, 584 insertions(+), 444 deletions(-) create mode 100644 internal/http_client.go create mode 100644 internal/mesh_object_client.go rename client_test.go => internal/mesh_object_client_test.go (81%) create mode 100644 internal/options.go diff --git a/buildingblock.go b/buildingblock.go index 3798b34..9652858 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + const ( MESH_BUILDING_BLOCK_IO_TYPE_STRING = "STRING" MESH_BUILDING_BLOCK_IO_TYPE_INTEGER = "INTEGER" @@ -71,21 +75,23 @@ type MeshBuildingBlockDefinitionRef struct { } type MeshBuildingBlockClient struct { - meshObjectClient[MeshBuildingBlock] + meshObject internal.MeshObjectClient[MeshBuildingBlock] } -func newBuildingBlockClient(c *httpClient) MeshBuildingBlockClient { - return MeshBuildingBlockClient{newMeshObjectClient[MeshBuildingBlock](c, "v1")} +func newBuildingBlockClient(httpClient *internal.HttpClient) MeshBuildingBlockClient { + return MeshBuildingBlockClient{ + meshObject: internal.NewMeshObjectClient[MeshBuildingBlock](httpClient, "v1"), + } } func (c MeshBuildingBlockClient) Read(uuid string) (*MeshBuildingBlock, error) { - return c.get(uuid) + return c.meshObject.Get(uuid) } func (c MeshBuildingBlockClient) Create(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { - return c.post(bb) + return c.meshObject.Post(bb) } func (c MeshBuildingBlockClient) Delete(uuid string) error { - return c.delete(uuid) + return c.meshObject.Delete(uuid) } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index b69cf73..456328d 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -3,9 +3,9 @@ package client import ( "context" "fmt" - "time" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "time" ) const ( @@ -66,23 +66,25 @@ type MeshBuildingBlockV2Status struct { } type MeshBuildingBlockV2Client struct { - meshObjectClient[MeshBuildingBlockV2] + meshObject internal.MeshObjectClient[MeshBuildingBlockV2] } -func newBuildingBlockV2Client(c *httpClient) MeshBuildingBlockV2Client { - return MeshBuildingBlockV2Client{newMeshObjectClient[MeshBuildingBlockV2](c, "v2-preview")} +func newBuildingBlockV2Client(httpClient *internal.HttpClient) MeshBuildingBlockV2Client { + return MeshBuildingBlockV2Client{ + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockV2](httpClient, "v2-preview"), + } } func (c MeshBuildingBlockV2Client) Read(uuid string) (*MeshBuildingBlockV2, error) { - return c.get(uuid) + return c.meshObject.Get(uuid) } func (c MeshBuildingBlockV2Client) Create(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { - return c.post(bb) + return c.meshObject.Post(bb) } func (c MeshBuildingBlockV2Client) Delete(uuid string) error { - return c.delete(uuid) + return c.meshObject.Delete(uuid) } // PollUntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) diff --git a/client.go b/client.go index 7b938c5..e742546 100644 --- a/client.go +++ b/client.go @@ -1,23 +1,12 @@ package client import ( - "bytes" - "encoding/json" - "errors" "fmt" - "io" - "log" "net/http" "net/url" - "reflect" - "slices" - "strings" "time" - "unicode" -) -var ( - errNotFound = errors.New("request failed with status Not Found (404)") + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) type MeshStackProviderClient struct { @@ -40,327 +29,32 @@ type MeshStackProviderClient struct { } func NewClient(rootUrl *url.URL, providerVersion, apiKey, apiSecret string) MeshStackProviderClient { - // Initialize httpClient for typed clients - c := &httpClient{ - Client: http.Client{Timeout: 5 * time.Minute}, - RootUrl: rootUrl, - ProviderVersion: providerVersion, - ApiKey: apiKey, - ApiSecret: apiSecret, - } - return MeshStackProviderClient{ - newBuildingBlockClient(c), - newBuildingBlockV2Client(c), - newIntegrationClient(c), - newLandingZoneClient(c), - newLocationClient(c), - newPaymentMethodClient(c), - newPlatformClient(c), - newProjectClient(c), - newProjectGroupBindingClient(c), - newProjectUserBindingClient(c), - newTagDefinitionClient(c), - newTenantClient(c), - newTenantV4Client(c), - newWorkspaceClient(c), - newWorkspaceGroupBindingClient(c), - newWorkspaceUserBindingClient(c), - } -} - -type httpClient struct { - http.Client - RootUrl *url.URL - ProviderVersion string - - ApiKey string - ApiSecret string - Token string - TokenExpiry time.Time -} - -type meshObjectClient[M any] struct { - *httpClient - Name, ApiVersion string - ApiUrl *url.URL -} - -func newMeshObjectClient[M any](client *httpClient, apiVersion string, explicitApiPaths ...string) meshObjectClient[M] { - name := inferMeshObjectName[M]() - - if len(explicitApiPaths) == 0 { - // infer API path from meshObject name by default (if nothing explicit is given) - explicitApiPaths = []string{strings.ToLower(pluralizeName(name))} - } - // also prepend the root path for all meshObjects - explicitApiPaths = slices.Insert(explicitApiPaths, 0, "/api/meshobjects") - apiUrl := client.RootUrl.JoinPath(explicitApiPaths...) - log.Printf("Using API at '%s' for meshObject '%s', version '%s'", apiUrl, name, apiVersion) - return meshObjectClient[M]{client, name, apiVersion, apiUrl} -} - -// inferMeshObjectName uses reflection to infer the meshObject name from the type parameter M. -// It converts the type name to camelCase (e.g., "MeshBuildingBlock" -> "meshBuildingBlock"). -func inferMeshObjectName[M any]() string { - var zero M - typeName := reflect.TypeOf(zero).Name() - return lowercaseFirst(typeName) -} - -func lowercaseFirst(s string) string { - if s == "" { - return s - } - runes := []rune(s) - runes[0] = unicode.ToLower(runes[0]) - return string(runes) -} - -func pluralizeName(name string) string { - return fmt.Sprintf("%ss", name) -} - -func (c meshObjectClient[M]) meshObjectMimeType() string { - return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", c.Name, c.ApiVersion) -} - -func (c meshObjectClient[M]) get(id string) (*M, error) { - body, err := c.doAuthenticatedRequest(http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) - if errors.Is(err, errNotFound) { - return nil, nil - } - return unmarshalBody[M](body, err) -} - -func (c meshObjectClient[M]) post(payload any) (*M, error) { - return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPost, c.ApiUrl, withPayload(payload, c.meshObjectMimeType()))) -} - -func (c meshObjectClient[M]) put(id string, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthenticatedRequest(http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.meshObjectMimeType()))) -} - -func (c meshObjectClient[M]) delete(id string) (err error) { - _, err = c.doAuthenticatedRequest(http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) - return -} - -func (c meshObjectClient[M]) list(options ...doRequestOption) ([]M, error) { - var result []M - embeddedKey := pluralizeName(c.Name) - pageNumber := 0 - for { - body, err := c.doAuthenticatedRequest(http.MethodGet, c.ApiUrl, append(options, - withAccept(c.meshObjectMimeType()), - withUrlQuery("page", pageNumber), - )...) - if err != nil { - return result, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err) - } - type paginatedResponse struct { - Embedded map[string][]M `json:"_embedded"` - Page struct { - TotalPages int `json:"totalPages"` - Number int `json:"number"` - } `json:"page"` - } - response, err := unmarshalBody[paginatedResponse](body, err) - if err != nil { - return result, fmt.Errorf("cannot unmarshal paginated response, page %d: %w", pageNumber, err) - } else if items, ok := response.Embedded[embeddedKey]; !ok { - return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) - } else { - result = append(result, items...) - } - // check if we've reached the end of pagination - if response.Page.Number >= response.Page.TotalPages-1 { - return result, nil - } - pageNumber++ - } -} - -func (c *httpClient) login() error { - loginApiUrl := c.RootUrl.JoinPath("/api/login") - - type loginRequest struct { - ClientId string `json:"clientId"` - ClientSecret string `json:"clientSecret"` - } - - type loginResponse struct { - Token string `json:"access_token"` - ExpireSec int `json:"expires_in"` - } - - loginResult, err := unmarshalBody[loginResponse](c.doRequest("POST", loginApiUrl, - withPayload(loginRequest{ClientId: c.ApiKey, ClientSecret: c.ApiSecret}, "application/json")), - ) - if err != nil { - return fmt.Errorf("login request to %s with API Key '%s' failed: %w", loginApiUrl, c.ApiKey, err) - } - - c.Token = fmt.Sprintf("Bearer %s", loginResult.Token) - c.TokenExpiry = time.Now().Add(time.Second * time.Duration(loginResult.ExpireSec)) - return nil -} + httpClient := &internal.HttpClient{ + Client: http.Client{Timeout: 5 * time.Minute}, + RootUrl: rootUrl, + UserAgent: fmt.Sprintf("terraform-provider-meshstack/%s", providerVersion), -func (c *httpClient) ensureValidToken() error { - if c.Token == "" || time.Now().Add(30*time.Second).After(c.TokenExpiry) { - return c.login() + // Putting authentication with meshStack API into HttpClient + // saves use from passing ApiKey/ApiSecret down to client factory methods below. + ApiKey: apiKey, + ApiSecret: apiSecret, } - return nil -} - -type doRequestOption func(opts *doRequestOptions) - -type requestModifier func(req *http.Request) - -type doRequestOptions struct { - urlQueryParams map[string]string - requestPayload any - requestModifiers []requestModifier -} - -func appendRequestModifier(modifier requestModifier) doRequestOption { - return func(opts *doRequestOptions) { - opts.requestModifiers = append(opts.requestModifiers, modifier) - } -} - -func withUrlQuery(key string, value any) doRequestOption { - return func(opts *doRequestOptions) { - var valueStr string - if stringerValue, ok := value.(fmt.Stringer); ok { - valueStr = stringerValue.String() - } else { - valueStr = fmt.Sprintf("%v", value) - } - if opts.urlQueryParams == nil { - opts.urlQueryParams = map[string]string{key: valueStr} - } else { - opts.urlQueryParams[key] = valueStr - } - } -} - -func withAccept(accept string) doRequestOption { - return withHeader("Accept", accept) -} - -func withHeader(key, value string) doRequestOption { - return appendRequestModifier(func(req *http.Request) { - req.Header.Set(key, value) - }) -} - -func withPayload(payload any, contentType string) doRequestOption { - return func(opts *doRequestOptions) { - // always provide Accept header with the same value as content-type, - // as meshObject API currently does not version that differently. - // that convention can still be overridden/broken by a later withAccept option - withAccept(contentType)(opts) - withHeader("Content-Type", contentType)(opts) - opts.requestPayload = payload - } -} - -func (c *httpClient) doRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { - // prepend (aka insert at 0) some default options such that given options may be overridden by caller - options = slices.Insert(options, 0, - withHeader("User-Agent", fmt.Sprintf("terraform-provider-meshstack/%s", c.ProviderVersion)), - ) - opts := doRequestOptions{} - for _, option := range options { - option(&opts) - } - req, err := c.buildRequest(method, *url, opts) - if err != nil { - return nil, err - } - res, err := c.Do(req) - if err != nil { - return nil, err - } - defer func() { - _ = res.Body.Close() - }() - log.Println(res) - return c.readBodyAndCheckSuccess(res) -} - -func (c *httpClient) readBodyAndCheckSuccess(res *http.Response) ([]byte, error) { - responseBody, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) - } - log.Printf("Got response body with %d bytes", len(responseBody)) - - if res.StatusCode >= 200 && res.StatusCode <= 299 { - return responseBody, nil - } - var errs []error - if res.StatusCode == http.StatusNotFound { - errs = append(errs, errNotFound) - } - errs = append(errs, - fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), - fmt.Errorf("error response: %s", string(responseBody)), - ) - // always return responseBody, even if the response is not successfully verified - // this allows clients to investigate the responseBody even further if desirable. - return responseBody, errors.Join(errs...) -} - -func (c *httpClient) buildRequest(method string, url url.URL, opts doRequestOptions) (*http.Request, error) { - if len(opts.urlQueryParams) > 0 { - query := url.Query() - for k, v := range opts.urlQueryParams { - query.Set(k, v) - } - // Note: url is not a pointer here, - // so we can safely update that struct field without propagating such change to the caller! - url.RawQuery = query.Encode() - } - - var requestBody io.ReadWriter - if opts.requestPayload != nil { - requestBody = new(bytes.Buffer) - if err := json.NewEncoder(requestBody).Encode(opts.requestPayload); err != nil { - return nil, fmt.Errorf("failed to encode request body payload: %w", err) - } - } - - req, err := http.NewRequest(method, url.String(), requestBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - for _, requestModifier := range opts.requestModifiers { - requestModifier(req) - } - return req, err -} - -func (c *httpClient) doAuthenticatedRequest(method string, url *url.URL, options ...doRequestOption) ([]byte, error) { - if err := c.ensureValidToken(); err != nil { - return nil, err - } - return c.doRequest(method, url, append(options, - appendRequestModifier(func(req *http.Request) { - // log request before adding Authorization header below - log.Println(req) - }), - withHeader("Authorization", c.Token), - )...) -} - -func unmarshalBody[T any](body []byte, err error) (*T, error) { - if err != nil { - return nil, err - } - var target T - if err := json.Unmarshal(body, &target); err != nil { - return nil, err + return MeshStackProviderClient{ + newBuildingBlockClient(httpClient), + newBuildingBlockV2Client(httpClient), + newIntegrationClient(httpClient), + newLandingZoneClient(httpClient), + newLocationClient(httpClient), + newPaymentMethodClient(httpClient), + newPlatformClient(httpClient), + newProjectClient(httpClient), + newProjectGroupBindingClient(httpClient), + newProjectUserBindingClient(httpClient), + newTagDefinitionClient(httpClient), + newTenantClient(httpClient), + newTenantV4Client(httpClient), + newWorkspaceClient(httpClient), + newWorkspaceGroupBindingClient(httpClient), + newWorkspaceUserBindingClient(httpClient), } - return &target, nil } diff --git a/integrations.go b/integrations.go index d9d192d..4cee539 100644 --- a/integrations.go +++ b/integrations.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshIntegration struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -80,11 +84,13 @@ type MeshAwsWifProvider struct { } type MeshIntegrationClient struct { - meshObjectClient[MeshIntegration] + meshObject internal.MeshObjectClient[MeshIntegration] } -func newIntegrationClient(c *httpClient) MeshIntegrationClient { - return MeshIntegrationClient{newMeshObjectClient[MeshIntegration](c, "v1-preview")} +func newIntegrationClient(httpClient *internal.HttpClient) MeshIntegrationClient { + return MeshIntegrationClient{ + meshObject: internal.NewMeshObjectClient[MeshIntegration](httpClient, "v1-preview"), + } } func (c MeshIntegrationClient) integrationId(workspace string, uuid string) string { @@ -92,9 +98,9 @@ func (c MeshIntegrationClient) integrationId(workspace string, uuid string) stri } func (c MeshIntegrationClient) Read(workspace string, uuid string) (*MeshIntegration, error) { - return c.get(c.integrationId(workspace, uuid)) + return c.meshObject.Get(c.integrationId(workspace, uuid)) } func (c MeshIntegrationClient) List() ([]MeshIntegration, error) { - return c.list() + return c.meshObject.List() } diff --git a/internal/http_client.go b/internal/http_client.go new file mode 100644 index 0000000..bb3580a --- /dev/null +++ b/internal/http_client.go @@ -0,0 +1,112 @@ +package internal + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "slices" + "time" +) + +var ( + errNotFound = errors.New("request failed with status Not Found (404)") +) + +// HttpClient wraps [http.Client] with convenient request handling thanks to RequestOption. +type HttpClient struct { + http.Client + RootUrl *url.URL + UserAgent string + + ApiKey string + ApiSecret string + Authorization string + AuthorizationExpiresAt time.Time +} + +func (c *HttpClient) doRequest(method string, url *url.URL, options ...RequestOption) ([]byte, error) { + options = slices.Insert(options, 0, + withHeader("User-Agent", c.UserAgent), + ) + opts := requestOptions{} + for _, option := range options { + option(&opts) + } + req, err := c.buildRequest(method, *url, opts) + if err != nil { + return nil, err + } + res, err := c.Do(req) + if err != nil { + return nil, err + } + defer func() { + _ = res.Body.Close() + }() + log.Println(res) + return c.readBodyAndCheckSuccess(res) +} + +func (c *HttpClient) readBodyAndCheckSuccess(res *http.Response) ([]byte, error) { + responseBody, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) + } + log.Printf("Got response body with %d bytes", len(responseBody)) + + if res.StatusCode >= 200 && res.StatusCode <= 299 { + return responseBody, nil + } + var errs []error + if res.StatusCode == http.StatusNotFound { + errs = append(errs, errNotFound) + } + errs = append(errs, + fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), + fmt.Errorf("error response: %s", string(responseBody)), + ) + return responseBody, errors.Join(errs...) +} + +func (c *HttpClient) buildRequest(method string, url url.URL, opts requestOptions) (*http.Request, error) { + if len(opts.urlQueryParams) > 0 { + query := url.Query() + for k, v := range opts.urlQueryParams { + query.Set(k, v) + } + url.RawQuery = query.Encode() + } + + var requestBody io.ReadWriter + if opts.requestPayload != nil { + requestBody = new(bytes.Buffer) + if err := json.NewEncoder(requestBody).Encode(opts.requestPayload); err != nil { + return nil, fmt.Errorf("failed to encode request body payload: %w", err) + } + } + + req, err := http.NewRequest(method, url.String(), requestBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + for _, requestModifier := range opts.requestModifiers { + requestModifier(req) + } + return req, err +} + +func unmarshalBody[T any](body []byte, err error) (*T, error) { + if err != nil { + return nil, err + } + var target T + if err := json.Unmarshal(body, &target); err != nil { + return nil, err + } + return &target, nil +} diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go new file mode 100644 index 0000000..b56a63f --- /dev/null +++ b/internal/mesh_object_client.go @@ -0,0 +1,170 @@ +package internal + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/url" + "reflect" + "slices" + "strings" + "time" + "unicode" +) + +// MeshObjectClient provides typed CRUD operations for meshStack API objects. +// It embeds [HttpClient] and adds meshObject-specific functionality including automatic +// MIME type handling and pagination. +// Also handles authentication in doAuthorizedRequest using the ApiKey/ApiSecret values, +// which are embedded in HttpClient for convenient construction with NewMeshObjectClient. +type MeshObjectClient[M any] struct { + *HttpClient + Name string + ApiVersion string + ApiUrl *url.URL +} + +// NewMeshObjectClient creates a new [MeshObjectClient] for a specific meshObject type with automatic URL path inference. +// The meshObject name is inferred from type M, and the API URL is constructed from explicitApiPaths or the pluralized type name. +func NewMeshObjectClient[M any](httpClient *HttpClient, apiVersion string, explicitApiPaths ...string) MeshObjectClient[M] { + name := inferMeshObjectName[M]() + + if len(explicitApiPaths) == 0 { + explicitApiPaths = []string{strings.ToLower(pluralizeName(name))} + } + explicitApiPaths = slices.Insert(explicitApiPaths, 0, "/api/meshobjects") + apiUrl := httpClient.RootUrl.JoinPath(explicitApiPaths...) + log.Printf("Using API at '%s' for meshObject '%s', version '%s'", apiUrl, name, apiVersion) + return MeshObjectClient[M]{httpClient, name, apiVersion, apiUrl} +} + +func inferMeshObjectName[M any]() string { + var zero M + typeName := reflect.TypeOf(zero).Name() + return lowercaseFirst(typeName) +} + +func lowercaseFirst(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToLower(runes[0]) + return string(runes) +} + +func pluralizeName(name string) string { + if strings.HasSuffix(name, "y") { + // this is ok, as we don't have meshObjects ending in 'y' yet, so take this shortcut + panic(fmt.Sprintf("Correctly pluralizing '%s' is not supported yet", name)) + } + return fmt.Sprintf("%ss", name) +} + +func (c MeshObjectClient[M]) meshObjectMimeType() string { + return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", c.Name, c.ApiVersion) +} + +// Get retrieves a meshObject by ID. Returns nil if not found. +func (c MeshObjectClient[M]) Get(id string) (*M, error) { + body, err := c.doAuthorizedRequest(http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) + if errors.Is(err, errNotFound) { + return nil, nil + } + return unmarshalBody[M](body, err) +} + +// Post creates a new meshObject with the given payload. +func (c MeshObjectClient[M]) Post(payload any) (*M, error) { + return unmarshalBody[M](c.doAuthorizedRequest(http.MethodPost, c.ApiUrl, withPayload(payload, c.meshObjectMimeType()))) +} + +// Put updates an existing meshObject by ID with the given payload. +func (c MeshObjectClient[M]) Put(id string, payload any) (*M, error) { + return unmarshalBody[M](c.doAuthorizedRequest(http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.meshObjectMimeType()))) +} + +// Delete removes a meshObject by ID. +func (c MeshObjectClient[M]) Delete(id string) (err error) { + _, err = c.doAuthorizedRequest(http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) + return +} + +// List retrieves all meshObjects with automatic pagination handling. +// Accepts optional [RequestOption] parameters for filtering and querying. +func (c MeshObjectClient[M]) List(options ...RequestOption) ([]M, error) { + var result []M + embeddedKey := pluralizeName(c.Name) + pageNumber := 0 + + for { + body, err := c.doAuthorizedRequest(http.MethodGet, c.ApiUrl, append(options, + withAccept(c.meshObjectMimeType()), + WithUrlQuery("page", pageNumber), + )...) + if err != nil { + return result, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err) + } + type paginatedResponse struct { + Embedded map[string][]M `json:"_embedded"` + Page struct { + TotalPages int `json:"totalPages"` + Number int `json:"number"` + } `json:"page"` + } + response, err := unmarshalBody[paginatedResponse](body, err) + if err != nil { + return result, fmt.Errorf("cannot unmarshal paginated response, page %d: %w", pageNumber, err) + } else if items, ok := response.Embedded[embeddedKey]; !ok { + return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) + } else { + result = append(result, items...) + } + if response.Page.Number >= response.Page.TotalPages-1 { + return result, nil + } + pageNumber++ + } +} + +func (c MeshObjectClient[M]) doAuthorizedRequest(method string, url *url.URL, options ...RequestOption) ([]byte, error) { + if err := c.ensureAuthorization(); err != nil { + return nil, err + } + return c.doRequest(method, url, append(options, + appendRequestModifier(func(req *http.Request) { + log.Println(req) + }), + withHeader("Authorization", c.Authorization), + )...) +} + +func (c MeshObjectClient[M]) ensureAuthorization() error { + if c.Authorization != "" && time.Until(c.AuthorizationExpiresAt) > 30*time.Second { + return nil + } + + loginApiUrl := c.RootUrl.JoinPath("/api/login") + + type loginRequest struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + } + + type loginResponse struct { + Token string `json:"access_token"` + ExpireSec int `json:"expires_in"` + } + + loginResult, err := unmarshalBody[loginResponse](c.doRequest("POST", loginApiUrl, + withPayload(loginRequest{ClientId: c.ApiKey, ClientSecret: c.ApiSecret}, "application/json")), + ) + if err != nil { + return fmt.Errorf("login request to %s with API Key '%s' failed: %w", loginApiUrl, c.ApiKey, err) + } + + c.Authorization = fmt.Sprintf("Bearer %s", loginResult.Token) + c.AuthorizationExpiresAt = time.Now().Add(time.Duration(loginResult.ExpireSec) * time.Second) + return nil +} diff --git a/client_test.go b/internal/mesh_object_client_test.go similarity index 81% rename from client_test.go rename to internal/mesh_object_client_test.go index 8b5f6f3..3afcdaa 100644 --- a/client_test.go +++ b/internal/mesh_object_client_test.go @@ -1,4 +1,4 @@ -package client +package internal import ( "testing" @@ -6,6 +6,20 @@ import ( "github.com/stretchr/testify/assert" ) +type MeshBuildingBlock struct{} +type MeshBuildingBlockV2 struct{} +type MeshProject struct{} +type MeshWorkspace struct{} +type MeshProjectBinding struct{} +type MeshProjectGroupBinding struct { + MeshProjectBinding +} +type MeshProjectUserBinding struct { + MeshProjectBinding +} +type MeshWorkspaceGroupBinding struct{} +type MeshWorkspaceUserBinding struct{} + func TestInferMeshObjectName(t *testing.T) { tests := []struct { name string diff --git a/internal/options.go b/internal/options.go new file mode 100644 index 0000000..4726dde --- /dev/null +++ b/internal/options.go @@ -0,0 +1,59 @@ +package internal + +import ( + "fmt" + "net/http" +) + +type ( + // RequestOption is a functional option for configuring HTTP requests. + RequestOption func(opts *requestOptions) + + requestOptions struct { + urlQueryParams map[string]string + requestPayload any + requestModifiers []requestModifier + } + requestModifier func(req *http.Request) +) + +// WithUrlQuery adds a URL query parameter to the request. +// The value is stringified using fmt.Stringer.String() if implemented, otherwise fmt.Sprintf("%v", value). +func WithUrlQuery(key string, value any) RequestOption { + return func(opts *requestOptions) { + var valueStr string + if stringerValue, ok := value.(fmt.Stringer); ok { + valueStr = stringerValue.String() + } else { + valueStr = fmt.Sprintf("%v", value) + } + if opts.urlQueryParams == nil { + opts.urlQueryParams = map[string]string{} + } + opts.urlQueryParams[key] = valueStr + } +} + +func appendRequestModifier(modifier requestModifier) RequestOption { + return func(opts *requestOptions) { + opts.requestModifiers = append(opts.requestModifiers, modifier) + } +} + +func withAccept(accept string) RequestOption { + return withHeader("Accept", accept) +} + +func withHeader(key, value string) RequestOption { + return appendRequestModifier(func(req *http.Request) { + req.Header.Set(key, value) + }) +} + +func withPayload(payload any, contentType string) RequestOption { + return func(opts *requestOptions) { + withAccept(contentType)(opts) + withHeader("Content-Type", contentType)(opts) + opts.requestPayload = payload + } +} diff --git a/landingzone.go b/landingzone.go index 9521d84..b1ee0b4 100644 --- a/landingzone.go +++ b/landingzone.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshLandingZone struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -60,25 +64,27 @@ type MeshLandingZoneCreate struct { } type MeshLandingZoneClient struct { - meshObjectClient[MeshLandingZone] + meshObject internal.MeshObjectClient[MeshLandingZone] } -func newLandingZoneClient(c *httpClient) MeshLandingZoneClient { - return MeshLandingZoneClient{newMeshObjectClient[MeshLandingZone](c, "v1-preview")} +func newLandingZoneClient(httpClient *internal.HttpClient) MeshLandingZoneClient { + return MeshLandingZoneClient{ + meshObject: internal.NewMeshObjectClient[MeshLandingZone](httpClient, "v1-preview"), + } } func (c MeshLandingZoneClient) Read(name string) (*MeshLandingZone, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshLandingZoneClient) Create(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - return c.post(landingZone) + return c.meshObject.Post(landingZone) } func (c MeshLandingZoneClient) Update(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - return c.put(name, landingZone) + return c.meshObject.Put(name, landingZone) } func (c MeshLandingZoneClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/location.go b/location.go index 3e8ced9..d37cc52 100644 --- a/location.go +++ b/location.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshLocation struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Metadata MeshLocationMetadata `json:"metadata" tfsdk:"metadata"` @@ -32,25 +36,27 @@ type MeshLocationCreateMetadata struct { } type MeshLocationClient struct { - meshObjectClient[MeshLocation] + meshObject internal.MeshObjectClient[MeshLocation] } -func newLocationClient(c *httpClient) MeshLocationClient { - return MeshLocationClient{newMeshObjectClient[MeshLocation](c, "v1-preview")} +func newLocationClient(httpClient *internal.HttpClient) MeshLocationClient { + return MeshLocationClient{ + meshObject: internal.NewMeshObjectClient[MeshLocation](httpClient, "v1-preview"), + } } func (c MeshLocationClient) Read(name string) (*MeshLocation, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshLocationClient) Create(location *MeshLocationCreate) (*MeshLocation, error) { - return c.post(location) + return c.meshObject.Post(location) } func (c MeshLocationClient) Update(name string, location *MeshLocationCreate) (*MeshLocation, error) { - return c.put(name, location) + return c.meshObject.Put(name, location) } func (c MeshLocationClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/payment_method.go b/payment_method.go index 6ce5378..d3ceefa 100644 --- a/payment_method.go +++ b/payment_method.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshPaymentMethod struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -33,25 +37,27 @@ type MeshPaymentMethodCreateMetadata struct { } type MeshPaymentMethodClient struct { - meshObjectClient[MeshPaymentMethod] + meshObject internal.MeshObjectClient[MeshPaymentMethod] } -func newPaymentMethodClient(c *httpClient) MeshPaymentMethodClient { - return MeshPaymentMethodClient{newMeshObjectClient[MeshPaymentMethod](c, "v2")} +func newPaymentMethodClient(httpClient *internal.HttpClient) MeshPaymentMethodClient { + return MeshPaymentMethodClient{ + meshObject: internal.NewMeshObjectClient[MeshPaymentMethod](httpClient, "v2"), + } } func (c MeshPaymentMethodClient) Read(workspace string, identifier string) (*MeshPaymentMethod, error) { - return c.get(identifier) + return c.meshObject.Get(identifier) } func (c MeshPaymentMethodClient) Create(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - return c.post(paymentMethod) + return c.meshObject.Post(paymentMethod) } func (c MeshPaymentMethodClient) Update(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - return c.put(identifier, paymentMethod) + return c.meshObject.Put(identifier, paymentMethod) } func (c MeshPaymentMethodClient) Delete(identifier string) error { - return c.delete(identifier) + return c.meshObject.Delete(identifier) } diff --git a/platform.go b/platform.go index 7d6a88f..0d33f12 100644 --- a/platform.go +++ b/platform.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshPlatform struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -104,25 +108,27 @@ type TagMapper struct { } type MeshPlatformClient struct { - meshObjectClient[MeshPlatform] + meshObject internal.MeshObjectClient[MeshPlatform] } -func newPlatformClient(c *httpClient) MeshPlatformClient { - return MeshPlatformClient{newMeshObjectClient[MeshPlatform](c, "v2-preview")} +func newPlatformClient(httpClient *internal.HttpClient) MeshPlatformClient { + return MeshPlatformClient{ + meshObject: internal.NewMeshObjectClient[MeshPlatform](httpClient, "v2-preview"), + } } func (c MeshPlatformClient) Read(uuid string) (*MeshPlatform, error) { - return c.get(uuid) + return c.meshObject.Get(uuid) } func (c MeshPlatformClient) Create(platform *MeshPlatformCreate) (*MeshPlatform, error) { - return c.post(platform) + return c.meshObject.Post(platform) } func (c MeshPlatformClient) Update(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { - return c.put(uuid, platform) + return c.meshObject.Put(uuid, platform) } func (c MeshPlatformClient) Delete(uuid string) error { - return c.delete(uuid) + return c.meshObject.Delete(uuid) } diff --git a/project.go b/project.go index d4b5f7b..6402b6a 100644 --- a/project.go +++ b/project.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshProject struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -32,11 +36,13 @@ type MeshProjectCreateMetadata struct { } type MeshProjectClient struct { - meshObjectClient[MeshProject] + meshObject internal.MeshObjectClient[MeshProject] } -func newProjectClient(c *httpClient) MeshProjectClient { - return MeshProjectClient{newMeshObjectClient[MeshProject](c, "v2")} +func newProjectClient(httpClient *internal.HttpClient) MeshProjectClient { + return MeshProjectClient{ + meshObject: internal.NewMeshObjectClient[MeshProject](httpClient, "v2"), + } } func (c MeshProjectClient) projectId(workspace string, name string) string { @@ -44,27 +50,27 @@ func (c MeshProjectClient) projectId(workspace string, name string) string { } func (c MeshProjectClient) Read(workspace string, name string) (*MeshProject, error) { - return c.get(c.projectId(workspace, name)) + return c.meshObject.Get(c.projectId(workspace, name)) } func (c MeshProjectClient) List(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { - options := []doRequestOption{ - withUrlQuery("workspaceIdentifier", workspaceIdentifier), + options := []internal.RequestOption{ + internal.WithUrlQuery("workspaceIdentifier", workspaceIdentifier), } if paymentMethodIdentifier != nil { - options = append(options, withUrlQuery("paymentIdentifier", *paymentMethodIdentifier)) + options = append(options, internal.WithUrlQuery("paymentIdentifier", *paymentMethodIdentifier)) } - return c.list(options...) + return c.meshObject.List(options...) } func (c MeshProjectClient) Create(project *MeshProjectCreate) (*MeshProject, error) { - return c.post(project) + return c.meshObject.Post(project) } func (c MeshProjectClient) Update(project *MeshProjectCreate) (*MeshProject, error) { - return c.put(c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) + return c.meshObject.Put(c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) } func (c MeshProjectClient) Delete(workspace string, name string) error { - return c.delete(c.projectId(workspace, name)) + return c.meshObject.Delete(c.projectId(workspace, name)) } diff --git a/project_group_binding.go b/project_group_binding.go index d3e7764..4d19df3 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -1,25 +1,31 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshProjectGroupBinding struct { MeshProjectBinding } type MeshProjectGroupBindingClient struct { - meshObjectClient[MeshProjectGroupBinding] + meshObject internal.MeshObjectClient[MeshProjectGroupBinding] } -func newProjectGroupBindingClient(c *httpClient) MeshProjectGroupBindingClient { - return MeshProjectGroupBindingClient{newMeshObjectClient[MeshProjectGroupBinding](c, "v3", "meshprojectbindings", "groupbindings")} +func newProjectGroupBindingClient(httpClient *internal.HttpClient) MeshProjectGroupBindingClient { + return MeshProjectGroupBindingClient{ + meshObject: internal.NewMeshObjectClient[MeshProjectGroupBinding](httpClient, "v3", "meshprojectbindings", "groupbindings"), + } } func (c MeshProjectGroupBindingClient) Read(name string) (*MeshProjectGroupBinding, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshProjectGroupBindingClient) Create(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { - return c.post(binding) + return c.meshObject.Post(binding) } func (c MeshProjectGroupBindingClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/project_user_binding.go b/project_user_binding.go index 51df0ba..334343d 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -1,25 +1,31 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshProjectUserBinding struct { MeshProjectBinding } type MeshProjectUserBindingClient struct { - meshObjectClient[MeshProjectUserBinding] + meshObject internal.MeshObjectClient[MeshProjectUserBinding] } -func newProjectUserBindingClient(c *httpClient) MeshProjectUserBindingClient { - return MeshProjectUserBindingClient{newMeshObjectClient[MeshProjectUserBinding](c, "v3", "meshprojectbindings", "userbindings")} +func newProjectUserBindingClient(httpClient *internal.HttpClient) MeshProjectUserBindingClient { + return MeshProjectUserBindingClient{ + meshObject: internal.NewMeshObjectClient[MeshProjectUserBinding](httpClient, "v3", "meshprojectbindings", "userbindings"), + } } func (c MeshProjectUserBindingClient) Read(name string) (*MeshProjectUserBinding, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshProjectUserBindingClient) Create(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - return c.post(binding) + return c.meshObject.Post(binding) } func (c MeshProjectUserBindingClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/tag_definition.go b/tag_definition.go index dfba7a2..18f32e1 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + const API_VERSION_TAG_DEFINITION = "v1" type MeshTagDefinition struct { @@ -64,29 +68,31 @@ type TagValueMultiSelect struct { } type MeshTagDefinitionClient struct { - meshObjectClient[MeshTagDefinition] + meshObject internal.MeshObjectClient[MeshTagDefinition] } -func newTagDefinitionClient(c *httpClient) MeshTagDefinitionClient { - return MeshTagDefinitionClient{newMeshObjectClient[MeshTagDefinition](c, "v1")} +func newTagDefinitionClient(httpClient *internal.HttpClient) MeshTagDefinitionClient { + return MeshTagDefinitionClient{ + meshObject: internal.NewMeshObjectClient[MeshTagDefinition](httpClient, "v1"), + } } func (c MeshTagDefinitionClient) List() ([]MeshTagDefinition, error) { - return c.list() + return c.meshObject.List() } func (c MeshTagDefinitionClient) Read(name string) (*MeshTagDefinition, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshTagDefinitionClient) Create(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - return c.post(tagDefinition) + return c.meshObject.Post(tagDefinition) } func (c MeshTagDefinitionClient) Update(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - return c.put(tagDefinition.Metadata.Name, tagDefinition) + return c.meshObject.Put(tagDefinition.Metadata.Name, tagDefinition) } func (c MeshTagDefinitionClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/tenant.go b/tenant.go index 7357113..ccd998d 100644 --- a/tenant.go +++ b/tenant.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshTenant struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -44,11 +48,13 @@ type MeshTenantCreateSpec struct { } type MeshTenantClient struct { - meshObjectClient[MeshTenant] + meshObject internal.MeshObjectClient[MeshTenant] } -func newTenantClient(c *httpClient) MeshTenantClient { - return MeshTenantClient{newMeshObjectClient[MeshTenant](c, "v3")} +func newTenantClient(httpClient *internal.HttpClient) MeshTenantClient { + return MeshTenantClient{ + meshObject: internal.NewMeshObjectClient[MeshTenant](httpClient, "v3"), + } } func (c MeshTenantClient) tenantId(workspace string, project string, platform string) string { @@ -56,13 +62,13 @@ func (c MeshTenantClient) tenantId(workspace string, project string, platform st } func (c MeshTenantClient) Read(workspace string, project string, platform string) (*MeshTenant, error) { - return c.get(c.tenantId(workspace, project, platform)) + return c.meshObject.Get(c.tenantId(workspace, project, platform)) } func (c MeshTenantClient) Create(tenant *MeshTenantCreate) (*MeshTenant, error) { - return c.post(tenant) + return c.meshObject.Post(tenant) } func (c MeshTenantClient) Delete(workspace string, project string, platform string) error { - return c.delete(c.tenantId(workspace, project, platform)) + return c.meshObject.Delete(c.tenantId(workspace, project, platform)) } diff --git a/tenant_v4.go b/tenant_v4.go index 6ba3f0c..65089fb 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -1,11 +1,14 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + import ( "context" "fmt" - "time" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "time" ) type MeshTenantV4 struct { @@ -57,23 +60,25 @@ type MeshTenantV4CreateSpec struct { } type MeshTenantV4Client struct { - meshObjectClient[MeshTenantV4] + meshObject internal.MeshObjectClient[MeshTenantV4] } -func newTenantV4Client(c *httpClient) MeshTenantV4Client { - return MeshTenantV4Client{newMeshObjectClient[MeshTenantV4](c, "v4-preview")} +func newTenantV4Client(httpClient *internal.HttpClient) MeshTenantV4Client { + return MeshTenantV4Client{ + meshObject: internal.NewMeshObjectClient[MeshTenantV4](httpClient, "v4-preview"), + } } func (c MeshTenantV4Client) Read(uuid string) (*MeshTenantV4, error) { - return c.get(uuid) + return c.meshObject.Get(uuid) } func (c MeshTenantV4Client) Create(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { - return c.post(tenant) + return c.meshObject.Post(tenant) } func (c MeshTenantV4Client) Delete(uuid string) error { - return c.delete(uuid) + return c.meshObject.Delete(uuid) } // PollUntilCreation polls a tenant until creation completes (platformTenantId is set) diff --git a/workspace.go b/workspace.go index 56f0f65..578643f 100644 --- a/workspace.go +++ b/workspace.go @@ -1,5 +1,9 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshWorkspace struct { ApiVersion string `json:"apiVersion" tfsdk:"api_version"` Kind string `json:"kind" tfsdk:"kind"` @@ -30,25 +34,27 @@ type MeshWorkspaceCreateMetadata struct { } type MeshWorkspaceClient struct { - meshObjectClient[MeshWorkspace] + meshObject internal.MeshObjectClient[MeshWorkspace] } -func newWorkspaceClient(c *httpClient) MeshWorkspaceClient { - return MeshWorkspaceClient{newMeshObjectClient[MeshWorkspace](c, "v2")} +func newWorkspaceClient(httpClient *internal.HttpClient) MeshWorkspaceClient { + return MeshWorkspaceClient{ + meshObject: internal.NewMeshObjectClient[MeshWorkspace](httpClient, "v2"), + } } func (c MeshWorkspaceClient) Read(name string) (*MeshWorkspace, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshWorkspaceClient) Create(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - return c.post(workspace) + return c.meshObject.Post(workspace) } func (c MeshWorkspaceClient) Update(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - return c.put(name, workspace) + return c.meshObject.Put(name, workspace) } func (c MeshWorkspaceClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 5d718f0..c947fb2 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -1,25 +1,31 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshWorkspaceGroupBinding struct { MeshWorkspaceBinding } type MeshWorkspaceGroupBindingClient struct { - meshObjectClient[MeshWorkspaceGroupBinding] + meshObject internal.MeshObjectClient[MeshWorkspaceGroupBinding] } -func newWorkspaceGroupBindingClient(c *httpClient) MeshWorkspaceGroupBindingClient { - return MeshWorkspaceGroupBindingClient{newMeshObjectClient[MeshWorkspaceGroupBinding](c, "v2", "meshworkspacebindings", "groupbindings")} +func newWorkspaceGroupBindingClient(httpClient *internal.HttpClient) MeshWorkspaceGroupBindingClient { + return MeshWorkspaceGroupBindingClient{ + meshObject: internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](httpClient, "v2", "meshworkspacebindings", "groupbindings"), + } } func (c MeshWorkspaceGroupBindingClient) Read(name string) (*MeshWorkspaceGroupBinding, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshWorkspaceGroupBindingClient) Create(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { - return c.post(binding) + return c.meshObject.Post(binding) } func (c MeshWorkspaceGroupBindingClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index c9c94ec..5e91796 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -1,25 +1,31 @@ package client +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + type MeshWorkspaceUserBinding struct { MeshWorkspaceBinding } type MeshWorkspaceUserBindingClient struct { - meshObjectClient[MeshWorkspaceUserBinding] + meshObject internal.MeshObjectClient[MeshWorkspaceUserBinding] } -func newWorkspaceUserBindingClient(c *httpClient) MeshWorkspaceUserBindingClient { - return MeshWorkspaceUserBindingClient{newMeshObjectClient[MeshWorkspaceUserBinding](c, "v2", "meshworkspacebindings", "userbindings")} +func newWorkspaceUserBindingClient(httpClient *internal.HttpClient) MeshWorkspaceUserBindingClient { + return MeshWorkspaceUserBindingClient{ + meshObject: internal.NewMeshObjectClient[MeshWorkspaceUserBinding](httpClient, "v2", "meshworkspacebindings", "userbindings"), + } } func (c MeshWorkspaceUserBindingClient) Read(name string) (*MeshWorkspaceUserBinding, error) { - return c.get(name) + return c.meshObject.Get(name) } func (c MeshWorkspaceUserBindingClient) Create(binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { - return c.post(binding) + return c.meshObject.Post(binding) } func (c MeshWorkspaceUserBindingClient) Delete(name string) error { - return c.delete(name) + return c.meshObject.Delete(name) } From 88c6d7161e54f612f9f9db162af28b8a3ce0f2c6 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Sat, 10 Jan 2026 14:13:29 +0100 Subject: [PATCH 077/200] refactor: use sub-clients in resources/data sources and helper configureProviderClient --- client.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client.go b/client.go index e742546..97cae04 100644 --- a/client.go +++ b/client.go @@ -1,7 +1,6 @@ package client import ( - "fmt" "net/http" "net/url" "time" @@ -9,7 +8,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) -type MeshStackProviderClient struct { +type Client struct { BuildingBlock MeshBuildingBlockClient BuildingBlockV2 MeshBuildingBlockV2Client Integration MeshIntegrationClient @@ -28,18 +27,18 @@ type MeshStackProviderClient struct { WorkspaceUserBinding MeshWorkspaceUserBindingClient } -func NewClient(rootUrl *url.URL, providerVersion, apiKey, apiSecret string) MeshStackProviderClient { +func New(rootUrl *url.URL, userAgent, apiKey, apiSecret string) Client { httpClient := &internal.HttpClient{ Client: http.Client{Timeout: 5 * time.Minute}, RootUrl: rootUrl, - UserAgent: fmt.Sprintf("terraform-provider-meshstack/%s", providerVersion), + UserAgent: userAgent, // Putting authentication with meshStack API into HttpClient // saves use from passing ApiKey/ApiSecret down to client factory methods below. ApiKey: apiKey, ApiSecret: apiSecret, } - return MeshStackProviderClient{ + return Client{ newBuildingBlockClient(httpClient), newBuildingBlockV2Client(httpClient), newIntegrationClient(httpClient), From c30d0925a2930bdb241f456c353de14e287c5d2d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 12 Jan 2026 09:09:48 +0100 Subject: [PATCH 078/200] feat: use gci to consistently format imports, improve Taskfile args handling --- buildingblock_v2.go | 4 +++- tenant_v4.go | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 456328d..4b0956e 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -3,9 +3,11 @@ package client import ( "context" "fmt" + "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "time" ) const ( diff --git a/tenant_v4.go b/tenant_v4.go index 65089fb..eccc4c5 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -1,14 +1,13 @@ package client -import ( - "github.com/meshcloud/terraform-provider-meshstack/client/internal" -) - import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) type MeshTenantV4 struct { From 86cdac2c4afd53777d0e6164134a2173345ad8e2 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 12 Jan 2026 21:02:45 +0100 Subject: [PATCH 079/200] feat: properly implement logging using tflog from SDK, remove versioned type suffix when inferring endpoint this does not spoil the client package with hashicorp dependencies and thanks to interfaces. The ctx must be passed around though, leading to some changes. also, we can use the context when running the request! --- buildingblock.go | 18 ++++--- buildingblock_v2.go | 28 +++++------ client.go | 35 ++++++------- client_logging.go | 11 ++++ integrations.go | 14 +++--- internal/http_client.go | 18 +++---- internal/logging.go | 78 +++++++++++++++++++++++++++++ internal/mesh_object_client.go | 51 +++++++++---------- internal/mesh_object_client_test.go | 48 +++--------------- landingzone.go | 22 ++++---- location.go | 22 ++++---- payment_method.go | 22 ++++---- platform.go | 22 ++++---- project.go | 26 +++++----- project_group_binding.go | 18 ++++--- project_user_binding.go | 18 ++++--- tag_definition.go | 26 +++++----- tenant.go | 18 ++++--- tenant_v4.go | 28 +++++------ workspace.go | 22 ++++---- workspace_group_binding.go | 18 ++++--- workspace_user_binding.go | 18 ++++--- 22 files changed, 331 insertions(+), 250 deletions(-) create mode 100644 client_logging.go create mode 100644 internal/logging.go diff --git a/buildingblock.go b/buildingblock.go index 9652858..f7c266e 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -78,20 +80,20 @@ type MeshBuildingBlockClient struct { meshObject internal.MeshObjectClient[MeshBuildingBlock] } -func newBuildingBlockClient(httpClient *internal.HttpClient) MeshBuildingBlockClient { +func newBuildingBlockClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockClient { return MeshBuildingBlockClient{ - meshObject: internal.NewMeshObjectClient[MeshBuildingBlock](httpClient, "v1"), + meshObject: internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1"), } } -func (c MeshBuildingBlockClient) Read(uuid string) (*MeshBuildingBlock, error) { - return c.meshObject.Get(uuid) +func (c MeshBuildingBlockClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) { + return c.meshObject.Get(ctx, uuid) } -func (c MeshBuildingBlockClient) Create(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { - return c.meshObject.Post(bb) +func (c MeshBuildingBlockClient) Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { + return c.meshObject.Post(ctx, bb) } -func (c MeshBuildingBlockClient) Delete(uuid string) error { - return c.meshObject.Delete(uuid) +func (c MeshBuildingBlockClient) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 4b0956e..c06409e 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -71,22 +71,22 @@ type MeshBuildingBlockV2Client struct { meshObject internal.MeshObjectClient[MeshBuildingBlockV2] } -func newBuildingBlockV2Client(httpClient *internal.HttpClient) MeshBuildingBlockV2Client { +func newBuildingBlockV2Client(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockV2Client { return MeshBuildingBlockV2Client{ - meshObject: internal.NewMeshObjectClient[MeshBuildingBlockV2](httpClient, "v2-preview"), + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview"), } } -func (c MeshBuildingBlockV2Client) Read(uuid string) (*MeshBuildingBlockV2, error) { - return c.meshObject.Get(uuid) +func (c MeshBuildingBlockV2Client) Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { + return c.meshObject.Get(ctx, uuid) } -func (c MeshBuildingBlockV2Client) Create(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { - return c.meshObject.Post(bb) +func (c MeshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { + return c.meshObject.Post(ctx, bb) } -func (c MeshBuildingBlockV2Client) Delete(uuid string) error { - return c.meshObject.Delete(uuid) +func (c MeshBuildingBlockV2Client) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) } // PollUntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) @@ -94,14 +94,14 @@ func (c MeshBuildingBlockV2Client) Delete(uuid string) error { func (c MeshBuildingBlockV2Client) PollUntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { var result *MeshBuildingBlockV2 - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCompletionFunc(uuid, &result)) + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCompletionFunc(ctx, uuid, &result)) return result, err } // waitForCompletionFunc returns a RetryFunc that checks building block completion status. -func (c MeshBuildingBlockV2Client) waitForCompletionFunc(uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { +func (c MeshBuildingBlockV2Client) waitForCompletionFunc(ctx context.Context, uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.Read(uuid) + current, err := c.Read(ctx, uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for completion: %w", err)) } @@ -128,13 +128,13 @@ func (c MeshBuildingBlockV2Client) waitForCompletionFunc(uuid string, result **M // PollUntilDeletion polls a building block until it is deleted (not found) // Returns nil on successful deletion or an error if polling fails or times out. func (c MeshBuildingBlockV2Client) PollUntilDeletion(ctx context.Context, uuid string) error { - return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(uuid)) + return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(ctx, uuid)) } // waitForDeletionFunc returns a RetryFunc that checks building block deletion status. -func (c MeshBuildingBlockV2Client) waitForDeletionFunc(uuid string) retry.RetryFunc { +func (c MeshBuildingBlockV2Client) waitForDeletionFunc(ctx context.Context, uuid string) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.Read(uuid) + current, err := c.Read(ctx, uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for deletion: %w", err)) } diff --git a/client.go b/client.go index 97cae04..3b1dee2 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,7 @@ package client import ( + "context" "net/http" "net/url" "time" @@ -27,7 +28,7 @@ type Client struct { WorkspaceUserBinding MeshWorkspaceUserBindingClient } -func New(rootUrl *url.URL, userAgent, apiKey, apiSecret string) Client { +func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string) Client { httpClient := &internal.HttpClient{ Client: http.Client{Timeout: 5 * time.Minute}, RootUrl: rootUrl, @@ -39,21 +40,21 @@ func New(rootUrl *url.URL, userAgent, apiKey, apiSecret string) Client { ApiSecret: apiSecret, } return Client{ - newBuildingBlockClient(httpClient), - newBuildingBlockV2Client(httpClient), - newIntegrationClient(httpClient), - newLandingZoneClient(httpClient), - newLocationClient(httpClient), - newPaymentMethodClient(httpClient), - newPlatformClient(httpClient), - newProjectClient(httpClient), - newProjectGroupBindingClient(httpClient), - newProjectUserBindingClient(httpClient), - newTagDefinitionClient(httpClient), - newTenantClient(httpClient), - newTenantV4Client(httpClient), - newWorkspaceClient(httpClient), - newWorkspaceGroupBindingClient(httpClient), - newWorkspaceUserBindingClient(httpClient), + newBuildingBlockClient(ctx, httpClient), + newBuildingBlockV2Client(ctx, httpClient), + newIntegrationClient(ctx, httpClient), + newLandingZoneClient(ctx, httpClient), + newLocationClient(ctx, httpClient), + newPaymentMethodClient(ctx, httpClient), + newPlatformClient(ctx, httpClient), + newProjectClient(ctx, httpClient), + newProjectGroupBindingClient(ctx, httpClient), + newProjectUserBindingClient(ctx, httpClient), + newTagDefinitionClient(ctx, httpClient), + newTenantClient(ctx, httpClient), + newTenantV4Client(ctx, httpClient), + newWorkspaceClient(ctx, httpClient), + newWorkspaceGroupBindingClient(ctx, httpClient), + newWorkspaceUserBindingClient(ctx, httpClient), } } diff --git a/client_logging.go b/client_logging.go new file mode 100644 index 0000000..ed6979a --- /dev/null +++ b/client_logging.go @@ -0,0 +1,11 @@ +package client + +import "github.com/meshcloud/terraform-provider-meshstack/client/internal" + +// Logger exposes logging for client operations within this package (including internal). +type Logger = internal.Logger + +// SetLogger allows setting the client logger. By default, no logging happens. +func SetLogger(logger Logger) { + internal.Log = logger +} diff --git a/integrations.go b/integrations.go index 4cee539..3f7e2cf 100644 --- a/integrations.go +++ b/integrations.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -87,9 +89,9 @@ type MeshIntegrationClient struct { meshObject internal.MeshObjectClient[MeshIntegration] } -func newIntegrationClient(httpClient *internal.HttpClient) MeshIntegrationClient { +func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) MeshIntegrationClient { return MeshIntegrationClient{ - meshObject: internal.NewMeshObjectClient[MeshIntegration](httpClient, "v1-preview"), + meshObject: internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1-preview"), } } @@ -97,10 +99,10 @@ func (c MeshIntegrationClient) integrationId(workspace string, uuid string) stri return workspace + "/" + uuid } -func (c MeshIntegrationClient) Read(workspace string, uuid string) (*MeshIntegration, error) { - return c.meshObject.Get(c.integrationId(workspace, uuid)) +func (c MeshIntegrationClient) Read(ctx context.Context, workspace string, uuid string) (*MeshIntegration, error) { + return c.meshObject.Get(ctx, c.integrationId(workspace, uuid)) } -func (c MeshIntegrationClient) List() ([]MeshIntegration, error) { - return c.meshObject.List() +func (c MeshIntegrationClient) List(ctx context.Context) ([]MeshIntegration, error) { + return c.meshObject.List(ctx) } diff --git a/internal/http_client.go b/internal/http_client.go index bb3580a..80a3d72 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -2,11 +2,11 @@ package internal import ( "bytes" + "context" "encoding/json" "errors" "fmt" "io" - "log" "net/http" "net/url" "slices" @@ -29,7 +29,7 @@ type HttpClient struct { AuthorizationExpiresAt time.Time } -func (c *HttpClient) doRequest(method string, url *url.URL, options ...RequestOption) ([]byte, error) { +func (c *HttpClient) doRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { options = slices.Insert(options, 0, withHeader("User-Agent", c.UserAgent), ) @@ -37,7 +37,7 @@ func (c *HttpClient) doRequest(method string, url *url.URL, options ...RequestOp for _, option := range options { option(&opts) } - req, err := c.buildRequest(method, *url, opts) + req, err := c.buildRequest(ctx, method, *url, opts) if err != nil { return nil, err } @@ -48,16 +48,15 @@ func (c *HttpClient) doRequest(method string, url *url.URL, options ...RequestOp defer func() { _ = res.Body.Close() }() - log.Println(res) - return c.readBodyAndCheckSuccess(res) + return c.readBodyAndCheckSuccess(ctx, res) } -func (c *HttpClient) readBodyAndCheckSuccess(res *http.Response) ([]byte, error) { +func (c *HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Response) ([]byte, error) { responseBody, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) } - log.Printf("Got response body with %d bytes", len(responseBody)) + Log.Debug(ctx, "response", "status", res.StatusCode, "body", loggedBody{bytes.NewBuffer(responseBody)}) if res.StatusCode >= 200 && res.StatusCode <= 299 { return responseBody, nil @@ -73,7 +72,7 @@ func (c *HttpClient) readBodyAndCheckSuccess(res *http.Response) ([]byte, error) return responseBody, errors.Join(errs...) } -func (c *HttpClient) buildRequest(method string, url url.URL, opts requestOptions) (*http.Request, error) { +func (c *HttpClient) buildRequest(ctx context.Context, method string, url url.URL, opts requestOptions) (*http.Request, error) { if len(opts.urlQueryParams) > 0 { query := url.Query() for k, v := range opts.urlQueryParams { @@ -90,13 +89,14 @@ func (c *HttpClient) buildRequest(method string, url url.URL, opts requestOption } } - req, err := http.NewRequest(method, url.String(), requestBody) + req, err := http.NewRequestWithContext(ctx, method, url.String(), requestBody) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } for _, requestModifier := range opts.requestModifiers { requestModifier(req) } + Log.Debug(ctx, "request", "url", req.URL.String(), "method", req.Method, "headers", loggedHeaders(req.Header), "body", loggedBody{requestBody}) return req, err } diff --git a/internal/logging.go b/internal/logging.go new file mode 100644 index 0000000..b45315e --- /dev/null +++ b/internal/logging.go @@ -0,0 +1,78 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "maps" + "net/http" + "slices" + "strings" +) + +var Log Logger = noopLogger{} + +// Logger only supports Debug and Info log levels. +type Logger interface { + Info(ctx context.Context, msg string, args ...any) + Debug(ctx context.Context, msg string, args ...any) +} + +type noopLogger struct{} + +func (n noopLogger) Info(context.Context, string, ...any) { + // do nothing +} + +func (n noopLogger) Debug(context.Context, string, ...any) { + // do nothing +} + +type loggedHeaders http.Header + +var _ fmt.Stringer = loggedHeaders(nil) + +func (l loggedHeaders) String() string { + var lines []string + for _, k := range slices.Sorted(maps.Keys(l)) { + for _, v := range l[k] { + // Avoid printing that longish JWT Bearer token (which is also a secret) + if k == "Authorization" { + v = "[REDACTED]" + } + lines = append(lines, fmt.Sprintf("%s=%s", k, v)) + } + } + return strings.Join(lines, "\n") +} + +type loggedBody struct { + io.Reader +} + +var _ fmt.Stringer = loggedBody{} + +func (l loggedBody) String() string { + if buffer, ok := l.Reader.(*bytes.Buffer); ok { + return bytesToPrettyJson(buffer.Bytes()) + } else if buffer == nil { + return "" + } + return fmt.Sprintf(" %v", l.Reader) +} + +func bytesToPrettyJson(data []byte) string { + if len(data) == 0 { + return "" + } + var decoded any + if err := json.Unmarshal(data, &decoded); err == nil { + if indented, err := json.MarshalIndent(decoded, "", " "); err == nil { + return string(indented) + } + } + // should never happen as we should only transfer JSON in request/responses + return fmt.Sprintf(" %s", len(data), string(data)) +} diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index b56a63f..96f4312 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -1,12 +1,13 @@ package internal import ( + "context" "errors" "fmt" - "log" "net/http" "net/url" "reflect" + "regexp" "slices" "strings" "time" @@ -27,22 +28,23 @@ type MeshObjectClient[M any] struct { // NewMeshObjectClient creates a new [MeshObjectClient] for a specific meshObject type with automatic URL path inference. // The meshObject name is inferred from type M, and the API URL is constructed from explicitApiPaths or the pluralized type name. -func NewMeshObjectClient[M any](httpClient *HttpClient, apiVersion string, explicitApiPaths ...string) MeshObjectClient[M] { - name := inferMeshObjectName[M]() +func NewMeshObjectClient[M any](ctx context.Context, httpClient *HttpClient, apiVersion string, explicitApiPaths ...string) MeshObjectClient[M] { + name, typeName := inferMeshObjectName[M]() if len(explicitApiPaths) == 0 { explicitApiPaths = []string{strings.ToLower(pluralizeName(name))} } explicitApiPaths = slices.Insert(explicitApiPaths, 0, "/api/meshobjects") apiUrl := httpClient.RootUrl.JoinPath(explicitApiPaths...) - log.Printf("Using API at '%s' for meshObject '%s', version '%s'", apiUrl, name, apiVersion) + Log.Info(ctx, fmt.Sprintf("initialized %s", typeName), "url", apiUrl.String(), "name", name, "version", apiVersion) return MeshObjectClient[M]{httpClient, name, apiVersion, apiUrl} } -func inferMeshObjectName[M any]() string { +func inferMeshObjectName[M any]() (name, typeName string) { var zero M - typeName := reflect.TypeOf(zero).Name() - return lowercaseFirst(typeName) + typeName = reflect.TypeOf(zero).Name() + name = lowercaseFirst(typeName) + return regexp.MustCompile(`V\d+$`).ReplaceAllString(name, ""), typeName } func lowercaseFirst(s string) string { @@ -67,8 +69,8 @@ func (c MeshObjectClient[M]) meshObjectMimeType() string { } // Get retrieves a meshObject by ID. Returns nil if not found. -func (c MeshObjectClient[M]) Get(id string) (*M, error) { - body, err := c.doAuthorizedRequest(http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) +func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (*M, error) { + body, err := c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) if errors.Is(err, errNotFound) { return nil, nil } @@ -76,30 +78,30 @@ func (c MeshObjectClient[M]) Get(id string) (*M, error) { } // Post creates a new meshObject with the given payload. -func (c MeshObjectClient[M]) Post(payload any) (*M, error) { - return unmarshalBody[M](c.doAuthorizedRequest(http.MethodPost, c.ApiUrl, withPayload(payload, c.meshObjectMimeType()))) +func (c MeshObjectClient[M]) Post(ctx context.Context, payload any) (*M, error) { + return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPost, c.ApiUrl, withPayload(payload, c.meshObjectMimeType()))) } // Put updates an existing meshObject by ID with the given payload. -func (c MeshObjectClient[M]) Put(id string, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthorizedRequest(http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.meshObjectMimeType()))) +func (c MeshObjectClient[M]) Put(ctx context.Context, id string, payload any) (*M, error) { + return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.meshObjectMimeType()))) } // Delete removes a meshObject by ID. -func (c MeshObjectClient[M]) Delete(id string) (err error) { - _, err = c.doAuthorizedRequest(http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) +func (c MeshObjectClient[M]) Delete(ctx context.Context, id string) (err error) { + _, err = c.doAuthorizedRequest(ctx, http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) return } // List retrieves all meshObjects with automatic pagination handling. // Accepts optional [RequestOption] parameters for filtering and querying. -func (c MeshObjectClient[M]) List(options ...RequestOption) ([]M, error) { +func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) ([]M, error) { var result []M embeddedKey := pluralizeName(c.Name) pageNumber := 0 for { - body, err := c.doAuthorizedRequest(http.MethodGet, c.ApiUrl, append(options, + body, err := c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl, append(options, withAccept(c.meshObjectMimeType()), WithUrlQuery("page", pageNumber), )...) @@ -128,19 +130,14 @@ func (c MeshObjectClient[M]) List(options ...RequestOption) ([]M, error) { } } -func (c MeshObjectClient[M]) doAuthorizedRequest(method string, url *url.URL, options ...RequestOption) ([]byte, error) { - if err := c.ensureAuthorization(); err != nil { +func (c MeshObjectClient[M]) doAuthorizedRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { + if err := c.ensureAuthorization(ctx); err != nil { return nil, err } - return c.doRequest(method, url, append(options, - appendRequestModifier(func(req *http.Request) { - log.Println(req) - }), - withHeader("Authorization", c.Authorization), - )...) + return c.doRequest(ctx, method, url, append(options, withHeader("Authorization", c.Authorization))...) } -func (c MeshObjectClient[M]) ensureAuthorization() error { +func (c MeshObjectClient[M]) ensureAuthorization(ctx context.Context) error { if c.Authorization != "" && time.Until(c.AuthorizationExpiresAt) > 30*time.Second { return nil } @@ -157,7 +154,7 @@ func (c MeshObjectClient[M]) ensureAuthorization() error { ExpireSec int `json:"expires_in"` } - loginResult, err := unmarshalBody[loginResponse](c.doRequest("POST", loginApiUrl, + loginResult, err := unmarshalBody[loginResponse](c.doRequest(ctx, "POST", loginApiUrl, withPayload(loginRequest{ClientId: c.ApiKey, ClientSecret: c.ApiSecret}, "application/json")), ) if err != nil { diff --git a/internal/mesh_object_client_test.go b/internal/mesh_object_client_test.go index 3afcdaa..4c9219c 100644 --- a/internal/mesh_object_client_test.go +++ b/internal/mesh_object_client_test.go @@ -8,22 +8,13 @@ import ( type MeshBuildingBlock struct{} type MeshBuildingBlockV2 struct{} -type MeshProject struct{} +type MeshTenantV4 struct{} type MeshWorkspace struct{} -type MeshProjectBinding struct{} -type MeshProjectGroupBinding struct { - MeshProjectBinding -} -type MeshProjectUserBinding struct { - MeshProjectBinding -} -type MeshWorkspaceGroupBinding struct{} -type MeshWorkspaceUserBinding struct{} func TestInferMeshObjectName(t *testing.T) { tests := []struct { name string - testFunc func() string + testFunc func() (string, string) expected string }{ { @@ -34,12 +25,7 @@ func TestInferMeshObjectName(t *testing.T) { { name: "MeshBuildingBlockV2", testFunc: inferMeshObjectName[MeshBuildingBlockV2], - expected: "meshBuildingBlockV2", - }, - { - name: "MeshProject", - testFunc: inferMeshObjectName[MeshProject], - expected: "meshProject", + expected: "meshBuildingBlock", }, { name: "MeshWorkspace", @@ -47,35 +33,15 @@ func TestInferMeshObjectName(t *testing.T) { expected: "meshWorkspace", }, { - name: "MeshProjectBinding", - testFunc: inferMeshObjectName[MeshProjectBinding], - expected: "meshProjectBinding", - }, - { - name: "MeshProjectGroupBinding (embedded struct)", - testFunc: inferMeshObjectName[MeshProjectGroupBinding], - expected: "meshProjectGroupBinding", - }, - { - name: "MeshProjectUserBinding (embedded struct)", - testFunc: inferMeshObjectName[MeshProjectUserBinding], - expected: "meshProjectUserBinding", - }, - { - name: "MeshWorkspaceGroupBinding (embedded struct)", - testFunc: inferMeshObjectName[MeshWorkspaceGroupBinding], - expected: "meshWorkspaceGroupBinding", - }, - { - name: "MeshWorkspaceUserBinding (embedded struct)", - testFunc: inferMeshObjectName[MeshWorkspaceUserBinding], - expected: "meshWorkspaceUserBinding", + name: "MeshTenantV4", + testFunc: inferMeshObjectName[MeshTenantV4], + expected: "meshTenant", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := tt.testFunc() + actual, _ := tt.testFunc() assert.Equal(t, tt.expected, actual) }) } diff --git a/landingzone.go b/landingzone.go index b1ee0b4..9081ee4 100644 --- a/landingzone.go +++ b/landingzone.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -67,24 +69,24 @@ type MeshLandingZoneClient struct { meshObject internal.MeshObjectClient[MeshLandingZone] } -func newLandingZoneClient(httpClient *internal.HttpClient) MeshLandingZoneClient { +func newLandingZoneClient(ctx context.Context, httpClient *internal.HttpClient) MeshLandingZoneClient { return MeshLandingZoneClient{ - meshObject: internal.NewMeshObjectClient[MeshLandingZone](httpClient, "v1-preview"), + meshObject: internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1-preview"), } } -func (c MeshLandingZoneClient) Read(name string) (*MeshLandingZone, error) { - return c.meshObject.Get(name) +func (c MeshLandingZoneClient) Read(ctx context.Context, name string) (*MeshLandingZone, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshLandingZoneClient) Create(landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - return c.meshObject.Post(landingZone) +func (c MeshLandingZoneClient) Create(ctx context.Context, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { + return c.meshObject.Post(ctx, landingZone) } -func (c MeshLandingZoneClient) Update(name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { - return c.meshObject.Put(name, landingZone) +func (c MeshLandingZoneClient) Update(ctx context.Context, name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { + return c.meshObject.Put(ctx, name, landingZone) } -func (c MeshLandingZoneClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshLandingZoneClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/location.go b/location.go index d37cc52..ee6dbd4 100644 --- a/location.go +++ b/location.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -39,24 +41,24 @@ type MeshLocationClient struct { meshObject internal.MeshObjectClient[MeshLocation] } -func newLocationClient(httpClient *internal.HttpClient) MeshLocationClient { +func newLocationClient(ctx context.Context, httpClient *internal.HttpClient) MeshLocationClient { return MeshLocationClient{ - meshObject: internal.NewMeshObjectClient[MeshLocation](httpClient, "v1-preview"), + meshObject: internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1-preview"), } } -func (c MeshLocationClient) Read(name string) (*MeshLocation, error) { - return c.meshObject.Get(name) +func (c MeshLocationClient) Read(ctx context.Context, name string) (*MeshLocation, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshLocationClient) Create(location *MeshLocationCreate) (*MeshLocation, error) { - return c.meshObject.Post(location) +func (c MeshLocationClient) Create(ctx context.Context, location *MeshLocationCreate) (*MeshLocation, error) { + return c.meshObject.Post(ctx, location) } -func (c MeshLocationClient) Update(name string, location *MeshLocationCreate) (*MeshLocation, error) { - return c.meshObject.Put(name, location) +func (c MeshLocationClient) Update(ctx context.Context, name string, location *MeshLocationCreate) (*MeshLocation, error) { + return c.meshObject.Put(ctx, name, location) } -func (c MeshLocationClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshLocationClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/payment_method.go b/payment_method.go index d3ceefa..7f9a041 100644 --- a/payment_method.go +++ b/payment_method.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -40,24 +42,24 @@ type MeshPaymentMethodClient struct { meshObject internal.MeshObjectClient[MeshPaymentMethod] } -func newPaymentMethodClient(httpClient *internal.HttpClient) MeshPaymentMethodClient { +func newPaymentMethodClient(ctx context.Context, httpClient *internal.HttpClient) MeshPaymentMethodClient { return MeshPaymentMethodClient{ - meshObject: internal.NewMeshObjectClient[MeshPaymentMethod](httpClient, "v2"), + meshObject: internal.NewMeshObjectClient[MeshPaymentMethod](ctx, httpClient, "v2"), } } -func (c MeshPaymentMethodClient) Read(workspace string, identifier string) (*MeshPaymentMethod, error) { - return c.meshObject.Get(identifier) +func (c MeshPaymentMethodClient) Read(ctx context.Context, workspace string, identifier string) (*MeshPaymentMethod, error) { + return c.meshObject.Get(ctx, identifier) } -func (c MeshPaymentMethodClient) Create(paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - return c.meshObject.Post(paymentMethod) +func (c MeshPaymentMethodClient) Create(ctx context.Context, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + return c.meshObject.Post(ctx, paymentMethod) } -func (c MeshPaymentMethodClient) Update(identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { - return c.meshObject.Put(identifier, paymentMethod) +func (c MeshPaymentMethodClient) Update(ctx context.Context, identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { + return c.meshObject.Put(ctx, identifier, paymentMethod) } -func (c MeshPaymentMethodClient) Delete(identifier string) error { - return c.meshObject.Delete(identifier) +func (c MeshPaymentMethodClient) Delete(ctx context.Context, identifier string) error { + return c.meshObject.Delete(ctx, identifier) } diff --git a/platform.go b/platform.go index 0d33f12..3900530 100644 --- a/platform.go +++ b/platform.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -111,24 +113,24 @@ type MeshPlatformClient struct { meshObject internal.MeshObjectClient[MeshPlatform] } -func newPlatformClient(httpClient *internal.HttpClient) MeshPlatformClient { +func newPlatformClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformClient { return MeshPlatformClient{ - meshObject: internal.NewMeshObjectClient[MeshPlatform](httpClient, "v2-preview"), + meshObject: internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2-preview"), } } -func (c MeshPlatformClient) Read(uuid string) (*MeshPlatform, error) { - return c.meshObject.Get(uuid) +func (c MeshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatform, error) { + return c.meshObject.Get(ctx, uuid) } -func (c MeshPlatformClient) Create(platform *MeshPlatformCreate) (*MeshPlatform, error) { - return c.meshObject.Post(platform) +func (c MeshPlatformClient) Create(ctx context.Context, platform *MeshPlatformCreate) (*MeshPlatform, error) { + return c.meshObject.Post(ctx, platform) } -func (c MeshPlatformClient) Update(uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { - return c.meshObject.Put(uuid, platform) +func (c MeshPlatformClient) Update(ctx context.Context, uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { + return c.meshObject.Put(ctx, uuid, platform) } -func (c MeshPlatformClient) Delete(uuid string) error { - return c.meshObject.Delete(uuid) +func (c MeshPlatformClient) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) } diff --git a/project.go b/project.go index 6402b6a..efdb4d9 100644 --- a/project.go +++ b/project.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -39,9 +41,9 @@ type MeshProjectClient struct { meshObject internal.MeshObjectClient[MeshProject] } -func newProjectClient(httpClient *internal.HttpClient) MeshProjectClient { +func newProjectClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectClient { return MeshProjectClient{ - meshObject: internal.NewMeshObjectClient[MeshProject](httpClient, "v2"), + meshObject: internal.NewMeshObjectClient[MeshProject](ctx, httpClient, "v2"), } } @@ -49,28 +51,28 @@ func (c MeshProjectClient) projectId(workspace string, name string) string { return workspace + "." + name } -func (c MeshProjectClient) Read(workspace string, name string) (*MeshProject, error) { - return c.meshObject.Get(c.projectId(workspace, name)) +func (c MeshProjectClient) Read(ctx context.Context, workspace string, name string) (*MeshProject, error) { + return c.meshObject.Get(ctx, c.projectId(workspace, name)) } -func (c MeshProjectClient) List(workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { +func (c MeshProjectClient) List(ctx context.Context, workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { options := []internal.RequestOption{ internal.WithUrlQuery("workspaceIdentifier", workspaceIdentifier), } if paymentMethodIdentifier != nil { options = append(options, internal.WithUrlQuery("paymentIdentifier", *paymentMethodIdentifier)) } - return c.meshObject.List(options...) + return c.meshObject.List(ctx, options...) } -func (c MeshProjectClient) Create(project *MeshProjectCreate) (*MeshProject, error) { - return c.meshObject.Post(project) +func (c MeshProjectClient) Create(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { + return c.meshObject.Post(ctx, project) } -func (c MeshProjectClient) Update(project *MeshProjectCreate) (*MeshProject, error) { - return c.meshObject.Put(c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) +func (c MeshProjectClient) Update(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { + return c.meshObject.Put(ctx, c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) } -func (c MeshProjectClient) Delete(workspace string, name string) error { - return c.meshObject.Delete(c.projectId(workspace, name)) +func (c MeshProjectClient) Delete(ctx context.Context, workspace string, name string) error { + return c.meshObject.Delete(ctx, c.projectId(workspace, name)) } diff --git a/project_group_binding.go b/project_group_binding.go index 4d19df3..69d36b3 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -12,20 +14,20 @@ type MeshProjectGroupBindingClient struct { meshObject internal.MeshObjectClient[MeshProjectGroupBinding] } -func newProjectGroupBindingClient(httpClient *internal.HttpClient) MeshProjectGroupBindingClient { +func newProjectGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectGroupBindingClient { return MeshProjectGroupBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshProjectGroupBinding](httpClient, "v3", "meshprojectbindings", "groupbindings"), + meshObject: internal.NewMeshObjectClient[MeshProjectGroupBinding](ctx, httpClient, "v3", "meshprojectbindings", "groupbindings"), } } -func (c MeshProjectGroupBindingClient) Read(name string) (*MeshProjectGroupBinding, error) { - return c.meshObject.Get(name) +func (c MeshProjectGroupBindingClient) Read(ctx context.Context, name string) (*MeshProjectGroupBinding, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshProjectGroupBindingClient) Create(binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { - return c.meshObject.Post(binding) +func (c MeshProjectGroupBindingClient) Create(ctx context.Context, binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { + return c.meshObject.Post(ctx, binding) } -func (c MeshProjectGroupBindingClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshProjectGroupBindingClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/project_user_binding.go b/project_user_binding.go index 334343d..64838ef 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -12,20 +14,20 @@ type MeshProjectUserBindingClient struct { meshObject internal.MeshObjectClient[MeshProjectUserBinding] } -func newProjectUserBindingClient(httpClient *internal.HttpClient) MeshProjectUserBindingClient { +func newProjectUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectUserBindingClient { return MeshProjectUserBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshProjectUserBinding](httpClient, "v3", "meshprojectbindings", "userbindings"), + meshObject: internal.NewMeshObjectClient[MeshProjectUserBinding](ctx, httpClient, "v3", "meshprojectbindings", "userbindings"), } } -func (c MeshProjectUserBindingClient) Read(name string) (*MeshProjectUserBinding, error) { - return c.meshObject.Get(name) +func (c MeshProjectUserBindingClient) Read(ctx context.Context, name string) (*MeshProjectUserBinding, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshProjectUserBindingClient) Create(binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { - return c.meshObject.Post(binding) +func (c MeshProjectUserBindingClient) Create(ctx context.Context, binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { + return c.meshObject.Post(ctx, binding) } -func (c MeshProjectUserBindingClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshProjectUserBindingClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/tag_definition.go b/tag_definition.go index 18f32e1..4c2edeb 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -71,28 +73,28 @@ type MeshTagDefinitionClient struct { meshObject internal.MeshObjectClient[MeshTagDefinition] } -func newTagDefinitionClient(httpClient *internal.HttpClient) MeshTagDefinitionClient { +func newTagDefinitionClient(ctx context.Context, httpClient *internal.HttpClient) MeshTagDefinitionClient { return MeshTagDefinitionClient{ - meshObject: internal.NewMeshObjectClient[MeshTagDefinition](httpClient, "v1"), + meshObject: internal.NewMeshObjectClient[MeshTagDefinition](ctx, httpClient, "v1"), } } -func (c MeshTagDefinitionClient) List() ([]MeshTagDefinition, error) { - return c.meshObject.List() +func (c MeshTagDefinitionClient) List(ctx context.Context) ([]MeshTagDefinition, error) { + return c.meshObject.List(ctx) } -func (c MeshTagDefinitionClient) Read(name string) (*MeshTagDefinition, error) { - return c.meshObject.Get(name) +func (c MeshTagDefinitionClient) Read(ctx context.Context, name string) (*MeshTagDefinition, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshTagDefinitionClient) Create(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - return c.meshObject.Post(tagDefinition) +func (c MeshTagDefinitionClient) Create(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { + return c.meshObject.Post(ctx, tagDefinition) } -func (c MeshTagDefinitionClient) Update(tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { - return c.meshObject.Put(tagDefinition.Metadata.Name, tagDefinition) +func (c MeshTagDefinitionClient) Update(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { + return c.meshObject.Put(ctx, tagDefinition.Metadata.Name, tagDefinition) } -func (c MeshTagDefinitionClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshTagDefinitionClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/tenant.go b/tenant.go index ccd998d..732fa22 100644 --- a/tenant.go +++ b/tenant.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -51,9 +53,9 @@ type MeshTenantClient struct { meshObject internal.MeshObjectClient[MeshTenant] } -func newTenantClient(httpClient *internal.HttpClient) MeshTenantClient { +func newTenantClient(ctx context.Context, httpClient *internal.HttpClient) MeshTenantClient { return MeshTenantClient{ - meshObject: internal.NewMeshObjectClient[MeshTenant](httpClient, "v3"), + meshObject: internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3"), } } @@ -61,14 +63,14 @@ func (c MeshTenantClient) tenantId(workspace string, project string, platform st return workspace + "." + project + "." + platform } -func (c MeshTenantClient) Read(workspace string, project string, platform string) (*MeshTenant, error) { - return c.meshObject.Get(c.tenantId(workspace, project, platform)) +func (c MeshTenantClient) Read(ctx context.Context, workspace string, project string, platform string) (*MeshTenant, error) { + return c.meshObject.Get(ctx, c.tenantId(workspace, project, platform)) } -func (c MeshTenantClient) Create(tenant *MeshTenantCreate) (*MeshTenant, error) { - return c.meshObject.Post(tenant) +func (c MeshTenantClient) Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) { + return c.meshObject.Post(ctx, tenant) } -func (c MeshTenantClient) Delete(workspace string, project string, platform string) error { - return c.meshObject.Delete(c.tenantId(workspace, project, platform)) +func (c MeshTenantClient) Delete(ctx context.Context, workspace string, project string, platform string) error { + return c.meshObject.Delete(ctx, c.tenantId(workspace, project, platform)) } diff --git a/tenant_v4.go b/tenant_v4.go index eccc4c5..bcb1e58 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -62,22 +62,22 @@ type MeshTenantV4Client struct { meshObject internal.MeshObjectClient[MeshTenantV4] } -func newTenantV4Client(httpClient *internal.HttpClient) MeshTenantV4Client { +func newTenantV4Client(ctx context.Context, httpClient *internal.HttpClient) MeshTenantV4Client { return MeshTenantV4Client{ - meshObject: internal.NewMeshObjectClient[MeshTenantV4](httpClient, "v4-preview"), + meshObject: internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview"), } } -func (c MeshTenantV4Client) Read(uuid string) (*MeshTenantV4, error) { - return c.meshObject.Get(uuid) +func (c MeshTenantV4Client) Read(ctx context.Context, uuid string) (*MeshTenantV4, error) { + return c.meshObject.Get(ctx, uuid) } -func (c MeshTenantV4Client) Create(tenant *MeshTenantV4Create) (*MeshTenantV4, error) { - return c.meshObject.Post(tenant) +func (c MeshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) { + return c.meshObject.Post(ctx, tenant) } -func (c MeshTenantV4Client) Delete(uuid string) error { - return c.meshObject.Delete(uuid) +func (c MeshTenantV4Client) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) } // PollUntilCreation polls a tenant until creation completes (platformTenantId is set) @@ -85,14 +85,14 @@ func (c MeshTenantV4Client) Delete(uuid string) error { func (c MeshTenantV4Client) PollUntilCreation(ctx context.Context, uuid string) (*MeshTenantV4, error) { var result *MeshTenantV4 - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCreationFunc(uuid, &result)) + err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCreationFunc(ctx, uuid, &result)) return result, err } // waitForCreationFunc returns a RetryFunc that checks tenant creation status. -func (c MeshTenantV4Client) waitForCreationFunc(uuid string, result **MeshTenantV4) retry.RetryFunc { +func (c MeshTenantV4Client) waitForCreationFunc(ctx context.Context, uuid string, result **MeshTenantV4) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.Read(uuid) + current, err := c.Read(ctx, uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for creation: %w", err)) } @@ -115,13 +115,13 @@ func (c MeshTenantV4Client) waitForCreationFunc(uuid string, result **MeshTenant // PollUntilDeletion polls a tenant until it is deleted (not found) // Returns nil on successful deletion or an error if polling fails or times out. func (c MeshTenantV4Client) PollUntilDeletion(ctx context.Context, uuid string) error { - return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(uuid)) + return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(ctx, uuid)) } // waitForDeletionFunc returns a RetryFunc that checks tenant deletion status. -func (c MeshTenantV4Client) waitForDeletionFunc(uuid string) retry.RetryFunc { +func (c MeshTenantV4Client) waitForDeletionFunc(ctx context.Context, uuid string) retry.RetryFunc { return func() *retry.RetryError { - current, err := c.Read(uuid) + current, err := c.Read(ctx, uuid) if err != nil { return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for deletion: %w", err)) } diff --git a/workspace.go b/workspace.go index 578643f..ca984c8 100644 --- a/workspace.go +++ b/workspace.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -37,24 +39,24 @@ type MeshWorkspaceClient struct { meshObject internal.MeshObjectClient[MeshWorkspace] } -func newWorkspaceClient(httpClient *internal.HttpClient) MeshWorkspaceClient { +func newWorkspaceClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceClient { return MeshWorkspaceClient{ - meshObject: internal.NewMeshObjectClient[MeshWorkspace](httpClient, "v2"), + meshObject: internal.NewMeshObjectClient[MeshWorkspace](ctx, httpClient, "v2"), } } -func (c MeshWorkspaceClient) Read(name string) (*MeshWorkspace, error) { - return c.meshObject.Get(name) +func (c MeshWorkspaceClient) Read(ctx context.Context, name string) (*MeshWorkspace, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshWorkspaceClient) Create(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - return c.meshObject.Post(workspace) +func (c MeshWorkspaceClient) Create(ctx context.Context, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + return c.meshObject.Post(ctx, workspace) } -func (c MeshWorkspaceClient) Update(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { - return c.meshObject.Put(name, workspace) +func (c MeshWorkspaceClient) Update(ctx context.Context, name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + return c.meshObject.Put(ctx, name, workspace) } -func (c MeshWorkspaceClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshWorkspaceClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index c947fb2..74d0f74 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -12,20 +14,20 @@ type MeshWorkspaceGroupBindingClient struct { meshObject internal.MeshObjectClient[MeshWorkspaceGroupBinding] } -func newWorkspaceGroupBindingClient(httpClient *internal.HttpClient) MeshWorkspaceGroupBindingClient { +func newWorkspaceGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceGroupBindingClient { return MeshWorkspaceGroupBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](httpClient, "v2", "meshworkspacebindings", "groupbindings"), + meshObject: internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](ctx, httpClient, "v2", "meshworkspacebindings", "groupbindings"), } } -func (c MeshWorkspaceGroupBindingClient) Read(name string) (*MeshWorkspaceGroupBinding, error) { - return c.meshObject.Get(name) +func (c MeshWorkspaceGroupBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceGroupBinding, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshWorkspaceGroupBindingClient) Create(binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { - return c.meshObject.Post(binding) +func (c MeshWorkspaceGroupBindingClient) Create(ctx context.Context, binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { + return c.meshObject.Post(ctx, binding) } -func (c MeshWorkspaceGroupBindingClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshWorkspaceGroupBindingClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index 5e91796..d63a7bf 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -1,6 +1,8 @@ package client import ( + "context" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -12,20 +14,20 @@ type MeshWorkspaceUserBindingClient struct { meshObject internal.MeshObjectClient[MeshWorkspaceUserBinding] } -func newWorkspaceUserBindingClient(httpClient *internal.HttpClient) MeshWorkspaceUserBindingClient { +func newWorkspaceUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceUserBindingClient { return MeshWorkspaceUserBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshWorkspaceUserBinding](httpClient, "v2", "meshworkspacebindings", "userbindings"), + meshObject: internal.NewMeshObjectClient[MeshWorkspaceUserBinding](ctx, httpClient, "v2", "meshworkspacebindings", "userbindings"), } } -func (c MeshWorkspaceUserBindingClient) Read(name string) (*MeshWorkspaceUserBinding, error) { - return c.meshObject.Get(name) +func (c MeshWorkspaceUserBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceUserBinding, error) { + return c.meshObject.Get(ctx, name) } -func (c MeshWorkspaceUserBindingClient) Create(binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { - return c.meshObject.Post(binding) +func (c MeshWorkspaceUserBindingClient) Create(ctx context.Context, binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { + return c.meshObject.Post(ctx, binding) } -func (c MeshWorkspaceUserBindingClient) Delete(name string) error { - return c.meshObject.Delete(name) +func (c MeshWorkspaceUserBindingClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) } From 276e69ad4f212121dd61d8ce66abc4a83a641800 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 13 Jan 2026 00:11:35 +0100 Subject: [PATCH 080/200] refactor: add util.PollAtMostFor(...).Until instead of cluttering up client package, fix depguard config --- buildingblock_v2.go | 86 ++++++++++++--------------------------------- tenant_v4.go | 72 +++++++++---------------------------- 2 files changed, 40 insertions(+), 118 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index c06409e..023c66e 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -3,9 +3,6 @@ package client import ( "context" "fmt" - "time" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -78,7 +75,13 @@ func newBuildingBlockV2Client(ctx context.Context, httpClient *internal.HttpClie } func (c MeshBuildingBlockV2Client) Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { - return c.meshObject.Get(ctx, uuid) + return c.ReadFunc(uuid)(ctx) +} + +func (c MeshBuildingBlockV2Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) { + return func(ctx context.Context) (*MeshBuildingBlockV2, error) { + return c.meshObject.Get(ctx, uuid) + } } func (c MeshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { @@ -89,67 +92,24 @@ func (c MeshBuildingBlockV2Client) Delete(ctx context.Context, uuid string) erro return c.meshObject.Delete(ctx, uuid) } -// PollUntilCompletion polls a building block until it reaches a terminal state (SUCCEEDED or FAILED) -// Returns the final building block state or an error if polling fails or times out. -func (c MeshBuildingBlockV2Client) PollUntilCompletion(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { - var result *MeshBuildingBlockV2 - - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCompletionFunc(ctx, uuid, &result)) - return result, err -} - -// waitForCompletionFunc returns a RetryFunc that checks building block completion status. -func (c MeshBuildingBlockV2Client) waitForCompletionFunc(ctx context.Context, uuid string, result **MeshBuildingBlockV2) retry.RetryFunc { - return func() *retry.RetryError { - current, err := c.Read(ctx, uuid) - if err != nil { - return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for completion: %w", err)) - } - - if current == nil { - return retry.NonRetryableError(fmt.Errorf("building block was not found while waiting for completion")) - } - *result = current - - // Check if we've reached a terminal state - status := current.Status.Status - switch status { - case BUILDING_BLOCK_STATUS_SUCCEEDED: - return nil // Success, stop retrying - case BUILDING_BLOCK_STATUS_FAILED: - return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state", uuid)) - } - - // Not done yet, continue polling - return retry.RetryableError(fmt.Errorf("waiting for building block %s to complete: currently in %s state", uuid, status)) +func (bb *MeshBuildingBlockV2) CreateSuccessful() (done bool, err error) { + switch { + case bb == nil: + err = fmt.Errorf("building block not found after creation") + case bb.Status.Status == BUILDING_BLOCK_STATUS_FAILED: + err = fmt.Errorf("building block %s reached FAILED state during creation, check the building block run logs in meshStack", bb.Metadata.Uuid) + case bb.Status.Status == BUILDING_BLOCK_STATUS_SUCCEEDED: + done = true } + return } -// PollUntilDeletion polls a building block until it is deleted (not found) -// Returns nil on successful deletion or an error if polling fails or times out. -func (c MeshBuildingBlockV2Client) PollUntilDeletion(ctx context.Context, uuid string) error { - return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(ctx, uuid)) -} - -// waitForDeletionFunc returns a RetryFunc that checks building block deletion status. -func (c MeshBuildingBlockV2Client) waitForDeletionFunc(ctx context.Context, uuid string) retry.RetryFunc { - return func() *retry.RetryError { - current, err := c.Read(ctx, uuid) - if err != nil { - return retry.NonRetryableError(fmt.Errorf("could not read building block status while waiting for deletion: %w", err)) - } - - // If building block is not found, deletion is complete - if current == nil { - return nil // Success, stop retrying - } - - // If building block is in FAILED state during deletion, consider it a terminal state - if current.Status.Status == BUILDING_BLOCK_STATUS_FAILED { - return retry.NonRetryableError(fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", uuid)) - } - - // Not done yet, continue polling - return retry.RetryableError(fmt.Errorf("waiting for building block %s to be deleted: currently in %s state", uuid, current.Status.Status)) +func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) { + switch { + case bb == nil: + done = true + case bb.Status.Status == BUILDING_BLOCK_STATUS_FAILED: + err = fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", bb.Metadata.Uuid) } + return } diff --git a/tenant_v4.go b/tenant_v4.go index bcb1e58..f60fa32 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -3,9 +3,6 @@ package client import ( "context" "fmt" - "time" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) @@ -69,7 +66,13 @@ func newTenantV4Client(ctx context.Context, httpClient *internal.HttpClient) Mes } func (c MeshTenantV4Client) Read(ctx context.Context, uuid string) (*MeshTenantV4, error) { - return c.meshObject.Get(ctx, uuid) + return c.ReadFunc(uuid)(ctx) +} + +func (c MeshTenantV4Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) { + return func(ctx context.Context) (*MeshTenantV4, error) { + return c.meshObject.Get(ctx, uuid) + } } func (c MeshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) { @@ -80,58 +83,17 @@ func (c MeshTenantV4Client) Delete(ctx context.Context, uuid string) error { return c.meshObject.Delete(ctx, uuid) } -// PollUntilCreation polls a tenant until creation completes (platformTenantId is set) -// Returns the final tenant state or an error if polling fails or times out. -func (c MeshTenantV4Client) PollUntilCreation(ctx context.Context, uuid string) (*MeshTenantV4, error) { - var result *MeshTenantV4 - - err := retry.RetryContext(ctx, 30*time.Minute, c.waitForCreationFunc(ctx, uuid, &result)) - return result, err -} - -// waitForCreationFunc returns a RetryFunc that checks tenant creation status. -func (c MeshTenantV4Client) waitForCreationFunc(ctx context.Context, uuid string, result **MeshTenantV4) retry.RetryFunc { - return func() *retry.RetryError { - current, err := c.Read(ctx, uuid) - if err != nil { - return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for creation: %w", err)) - } - - if current == nil { - return retry.NonRetryableError(fmt.Errorf("tenant was not found while waiting for creation")) - } - - // Check if creation is complete (platformTenantId is set) - if current.Spec.PlatformTenantId != nil && *current.Spec.PlatformTenantId != "" { - *result = current - return nil // Success, stop retrying - } - - // Not done yet, continue polling - return retry.RetryableError(fmt.Errorf("waiting for tenant %s creation to complete: platformTenantId not yet set", uuid)) +func (tenant *MeshTenantV4) CreationSuccessful() (done bool, err error) { + switch { + case tenant == nil: + err = fmt.Errorf("tenant not found after creation") + case tenant.Spec.PlatformTenantId != nil && *tenant.Spec.PlatformTenantId != "": + // Creation is complete (platformTenantId is set and not empty) + done = true } + return } -// PollUntilDeletion polls a tenant until it is deleted (not found) -// Returns nil on successful deletion or an error if polling fails or times out. -func (c MeshTenantV4Client) PollUntilDeletion(ctx context.Context, uuid string) error { - return retry.RetryContext(ctx, 30*time.Minute, c.waitForDeletionFunc(ctx, uuid)) -} - -// waitForDeletionFunc returns a RetryFunc that checks tenant deletion status. -func (c MeshTenantV4Client) waitForDeletionFunc(ctx context.Context, uuid string) retry.RetryFunc { - return func() *retry.RetryError { - current, err := c.Read(ctx, uuid) - if err != nil { - return retry.NonRetryableError(fmt.Errorf("could not read tenant status while waiting for deletion: %w", err)) - } - - // If tenant is not found, deletion is complete - if current == nil { - return nil // Success, stop retrying - } - - // Not done yet, continue polling - return retry.RetryableError(fmt.Errorf("waiting for tenant %s to be deleted: still present", uuid)) - } +func (tenant *MeshTenantV4) DeletionSuccessful() (done bool, err error) { + return tenant == nil, nil } From 5188e2dc1bedb89cfe43232da93b927ef8547167 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 13 Jan 2026 10:43:33 +0100 Subject: [PATCH 081/200] refactor: rename resource/datasource struct client fields (resolves review comment) --- buildingblock.go | 4 +--- buildingblock_v2.go | 4 +--- integrations.go | 4 +--- landingzone.go | 4 +--- location.go | 4 +--- payment_method.go | 4 +--- platform.go | 4 +--- project.go | 4 +--- project_group_binding.go | 4 +--- project_user_binding.go | 4 +--- tag_definition.go | 4 +--- tenant.go | 4 +--- tenant_v4.go | 4 +--- workspace.go | 4 +--- workspace_group_binding.go | 4 +--- workspace_user_binding.go | 4 +--- 16 files changed, 16 insertions(+), 48 deletions(-) diff --git a/buildingblock.go b/buildingblock.go index f7c266e..c89d606 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -81,9 +81,7 @@ type MeshBuildingBlockClient struct { } func newBuildingBlockClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockClient { - return MeshBuildingBlockClient{ - meshObject: internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1"), - } + return MeshBuildingBlockClient{internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1")} } func (c MeshBuildingBlockClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) { diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 023c66e..fa3a3d5 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -69,9 +69,7 @@ type MeshBuildingBlockV2Client struct { } func newBuildingBlockV2Client(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockV2Client { - return MeshBuildingBlockV2Client{ - meshObject: internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview"), - } + return MeshBuildingBlockV2Client{internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview")} } func (c MeshBuildingBlockV2Client) Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { diff --git a/integrations.go b/integrations.go index 3f7e2cf..df2643e 100644 --- a/integrations.go +++ b/integrations.go @@ -90,9 +90,7 @@ type MeshIntegrationClient struct { } func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) MeshIntegrationClient { - return MeshIntegrationClient{ - meshObject: internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1-preview"), - } + return MeshIntegrationClient{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1-preview")} } func (c MeshIntegrationClient) integrationId(workspace string, uuid string) string { diff --git a/landingzone.go b/landingzone.go index 9081ee4..6588d19 100644 --- a/landingzone.go +++ b/landingzone.go @@ -70,9 +70,7 @@ type MeshLandingZoneClient struct { } func newLandingZoneClient(ctx context.Context, httpClient *internal.HttpClient) MeshLandingZoneClient { - return MeshLandingZoneClient{ - meshObject: internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1-preview"), - } + return MeshLandingZoneClient{internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1-preview")} } func (c MeshLandingZoneClient) Read(ctx context.Context, name string) (*MeshLandingZone, error) { diff --git a/location.go b/location.go index ee6dbd4..3656744 100644 --- a/location.go +++ b/location.go @@ -42,9 +42,7 @@ type MeshLocationClient struct { } func newLocationClient(ctx context.Context, httpClient *internal.HttpClient) MeshLocationClient { - return MeshLocationClient{ - meshObject: internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1-preview"), - } + return MeshLocationClient{internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1-preview")} } func (c MeshLocationClient) Read(ctx context.Context, name string) (*MeshLocation, error) { diff --git a/payment_method.go b/payment_method.go index 7f9a041..98111ff 100644 --- a/payment_method.go +++ b/payment_method.go @@ -43,9 +43,7 @@ type MeshPaymentMethodClient struct { } func newPaymentMethodClient(ctx context.Context, httpClient *internal.HttpClient) MeshPaymentMethodClient { - return MeshPaymentMethodClient{ - meshObject: internal.NewMeshObjectClient[MeshPaymentMethod](ctx, httpClient, "v2"), - } + return MeshPaymentMethodClient{internal.NewMeshObjectClient[MeshPaymentMethod](ctx, httpClient, "v2")} } func (c MeshPaymentMethodClient) Read(ctx context.Context, workspace string, identifier string) (*MeshPaymentMethod, error) { diff --git a/platform.go b/platform.go index 3900530..bd7b7fc 100644 --- a/platform.go +++ b/platform.go @@ -114,9 +114,7 @@ type MeshPlatformClient struct { } func newPlatformClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformClient { - return MeshPlatformClient{ - meshObject: internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2-preview"), - } + return MeshPlatformClient{internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2-preview")} } func (c MeshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatform, error) { diff --git a/project.go b/project.go index efdb4d9..d8a2fba 100644 --- a/project.go +++ b/project.go @@ -42,9 +42,7 @@ type MeshProjectClient struct { } func newProjectClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectClient { - return MeshProjectClient{ - meshObject: internal.NewMeshObjectClient[MeshProject](ctx, httpClient, "v2"), - } + return MeshProjectClient{internal.NewMeshObjectClient[MeshProject](ctx, httpClient, "v2")} } func (c MeshProjectClient) projectId(workspace string, name string) string { diff --git a/project_group_binding.go b/project_group_binding.go index 69d36b3..916d0e6 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -15,9 +15,7 @@ type MeshProjectGroupBindingClient struct { } func newProjectGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectGroupBindingClient { - return MeshProjectGroupBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshProjectGroupBinding](ctx, httpClient, "v3", "meshprojectbindings", "groupbindings"), - } + return MeshProjectGroupBindingClient{internal.NewMeshObjectClient[MeshProjectGroupBinding](ctx, httpClient, "v3", "meshprojectbindings", "groupbindings")} } func (c MeshProjectGroupBindingClient) Read(ctx context.Context, name string) (*MeshProjectGroupBinding, error) { diff --git a/project_user_binding.go b/project_user_binding.go index 64838ef..2ddcd6b 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -15,9 +15,7 @@ type MeshProjectUserBindingClient struct { } func newProjectUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectUserBindingClient { - return MeshProjectUserBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshProjectUserBinding](ctx, httpClient, "v3", "meshprojectbindings", "userbindings"), - } + return MeshProjectUserBindingClient{internal.NewMeshObjectClient[MeshProjectUserBinding](ctx, httpClient, "v3", "meshprojectbindings", "userbindings")} } func (c MeshProjectUserBindingClient) Read(ctx context.Context, name string) (*MeshProjectUserBinding, error) { diff --git a/tag_definition.go b/tag_definition.go index 4c2edeb..5b78ca9 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -74,9 +74,7 @@ type MeshTagDefinitionClient struct { } func newTagDefinitionClient(ctx context.Context, httpClient *internal.HttpClient) MeshTagDefinitionClient { - return MeshTagDefinitionClient{ - meshObject: internal.NewMeshObjectClient[MeshTagDefinition](ctx, httpClient, "v1"), - } + return MeshTagDefinitionClient{internal.NewMeshObjectClient[MeshTagDefinition](ctx, httpClient, "v1")} } func (c MeshTagDefinitionClient) List(ctx context.Context) ([]MeshTagDefinition, error) { diff --git a/tenant.go b/tenant.go index 732fa22..c68e9fb 100644 --- a/tenant.go +++ b/tenant.go @@ -54,9 +54,7 @@ type MeshTenantClient struct { } func newTenantClient(ctx context.Context, httpClient *internal.HttpClient) MeshTenantClient { - return MeshTenantClient{ - meshObject: internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3"), - } + return MeshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3")} } func (c MeshTenantClient) tenantId(workspace string, project string, platform string) string { diff --git a/tenant_v4.go b/tenant_v4.go index f60fa32..4d6f317 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -60,9 +60,7 @@ type MeshTenantV4Client struct { } func newTenantV4Client(ctx context.Context, httpClient *internal.HttpClient) MeshTenantV4Client { - return MeshTenantV4Client{ - meshObject: internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview"), - } + return MeshTenantV4Client{internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview")} } func (c MeshTenantV4Client) Read(ctx context.Context, uuid string) (*MeshTenantV4, error) { diff --git a/workspace.go b/workspace.go index ca984c8..e019308 100644 --- a/workspace.go +++ b/workspace.go @@ -40,9 +40,7 @@ type MeshWorkspaceClient struct { } func newWorkspaceClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceClient { - return MeshWorkspaceClient{ - meshObject: internal.NewMeshObjectClient[MeshWorkspace](ctx, httpClient, "v2"), - } + return MeshWorkspaceClient{internal.NewMeshObjectClient[MeshWorkspace](ctx, httpClient, "v2")} } func (c MeshWorkspaceClient) Read(ctx context.Context, name string) (*MeshWorkspace, error) { diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 74d0f74..e4404a6 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -15,9 +15,7 @@ type MeshWorkspaceGroupBindingClient struct { } func newWorkspaceGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceGroupBindingClient { - return MeshWorkspaceGroupBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](ctx, httpClient, "v2", "meshworkspacebindings", "groupbindings"), - } + return MeshWorkspaceGroupBindingClient{internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](ctx, httpClient, "v2", "meshworkspacebindings", "groupbindings")} } func (c MeshWorkspaceGroupBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceGroupBinding, error) { diff --git a/workspace_user_binding.go b/workspace_user_binding.go index d63a7bf..eb552d1 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -15,9 +15,7 @@ type MeshWorkspaceUserBindingClient struct { } func newWorkspaceUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceUserBindingClient { - return MeshWorkspaceUserBindingClient{ - meshObject: internal.NewMeshObjectClient[MeshWorkspaceUserBinding](ctx, httpClient, "v2", "meshworkspacebindings", "userbindings"), - } + return MeshWorkspaceUserBindingClient{internal.NewMeshObjectClient[MeshWorkspaceUserBinding](ctx, httpClient, "v2", "meshworkspacebindings", "userbindings")} } func (c MeshWorkspaceUserBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceUserBinding, error) { From 234279bb06f0ae58817ca44afd933093d8f7beb7 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 16 Jan 2026 15:53:05 +0100 Subject: [PATCH 082/200] feat: platform type data sources and resource --- client.go | 2 ++ platform_type.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 platform_type.go diff --git a/client.go b/client.go index 3b1dee2..70ffc31 100644 --- a/client.go +++ b/client.go @@ -26,6 +26,7 @@ type Client struct { Workspace MeshWorkspaceClient WorkspaceGroupBinding MeshWorkspaceGroupBindingClient WorkspaceUserBinding MeshWorkspaceUserBindingClient + PlatformType MeshPlatformTypeClient } func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string) Client { @@ -56,5 +57,6 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str newWorkspaceClient(ctx, httpClient), newWorkspaceGroupBindingClient(ctx, httpClient), newWorkspaceUserBindingClient(ctx, httpClient), + newPlatformTypeClient(ctx, httpClient), } } diff --git a/platform_type.go b/platform_type.go new file mode 100644 index 0000000..cfabea6 --- /dev/null +++ b/platform_type.go @@ -0,0 +1,73 @@ +package client + +import ( + "context" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + +type MeshPlatformType struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshPlatformTypeMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPlatformTypeMetadata struct { + Name string `json:"name" tfsdk:"name"` + CreatedOn *string `json:"createdOn" tfsdk:"created_on"` + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` +} + +type MeshPlatformTypeSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Category string `json:"category" tfsdk:"category"` + DefaultEndpoint *string `json:"defaultEndpoint,omitempty" tfsdk:"default_endpoint"` + Icon string `json:"icon" tfsdk:"icon"` +} + +type MeshPlatformTypeCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshPlatformTypeCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` +} + +type MeshPlatformTypeCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` +} + +type MeshPlatformTypeClient struct { + meshObject internal.MeshObjectClient[MeshPlatformType] +} + +func newPlatformTypeClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformTypeClient { + return MeshPlatformTypeClient{internal.NewMeshObjectClient[MeshPlatformType](ctx, httpClient, "v1-preview")} +} + +func (c MeshPlatformTypeClient) Read(ctx context.Context, identifier string) (*MeshPlatformType, error) { + return c.meshObject.Get(ctx, identifier) +} + +func (c MeshPlatformTypeClient) Create(ctx context.Context, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { + return c.meshObject.Post(ctx, platformType) +} + +func (c MeshPlatformTypeClient) Update(ctx context.Context, name string, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { + return c.meshObject.Put(ctx, name, platformType) +} + +func (c MeshPlatformTypeClient) Delete(ctx context.Context, name string) error { + return c.meshObject.Delete(ctx, name) +} + +func (c MeshPlatformTypeClient) List(ctx context.Context, category *string, lifecycleStatus *string) ([]MeshPlatformType, error) { + var options []internal.RequestOption + if category != nil { + options = append(options, internal.WithUrlQuery("category", *category)) + } + if lifecycleStatus != nil { + options = append(options, internal.WithUrlQuery("lifecycleStatus", *lifecycleStatus)) + } + return c.meshObject.List(ctx, options...) +} From 1eb24de91178857a8a86b23d0f074241926bc62b Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Tue, 20 Jan 2026 09:39:12 +0100 Subject: [PATCH 083/200] feat: add status field to platform type --- platform_type.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/platform_type.go b/platform_type.go index cfabea6..2cae4fb 100644 --- a/platform_type.go +++ b/platform_type.go @@ -11,6 +11,11 @@ type MeshPlatformType struct { Kind string `json:"kind" tfsdk:"kind"` Metadata MeshPlatformTypeMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` + Status MeshPlatformTypeStatus `json:"status" tfsdk:"status"` +} + +type MeshPlatformTypeStatus struct { + LifecycleState string `json:"lifecycleState" tfsdk:"lifecycle_state"` } type MeshPlatformTypeMetadata struct { From c073f53e3ddbce48c054097bd8a3c47a841651c7 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 21 Jan 2026 09:49:42 +0100 Subject: [PATCH 084/200] fix: upstream structure has changed --- platform_type.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platform_type.go b/platform_type.go index 2cae4fb..366cb19 100644 --- a/platform_type.go +++ b/platform_type.go @@ -15,7 +15,11 @@ type MeshPlatformType struct { } type MeshPlatformTypeStatus struct { - LifecycleState string `json:"lifecycleState" tfsdk:"lifecycle_state"` + Lifecycle MeshPlatformTypeLifecycle `json:"lifecycle" tfsdk:"lifecycle"` +} + +type MeshPlatformTypeLifecycle struct { + State string `json:"state" tfsdk:"state"` } type MeshPlatformTypeMetadata struct { From 0b94a1b302a6b33847993e30cc5dd6bf1c9655fb Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Mon, 19 Jan 2026 17:04:05 +0100 Subject: [PATCH 085/200] feat: login via api token --- client.go | 43 ++++++++++++++++++++++++++++++- client_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 client_test.go diff --git a/client.go b/client.go index 70ffc31..3d48749 100644 --- a/client.go +++ b/client.go @@ -2,8 +2,12 @@ package client import ( "context" + "encoding/base64" + "encoding/json" + "fmt" "net/http" "net/url" + "strings" "time" "github.com/meshcloud/terraform-provider-meshstack/client/internal" @@ -29,7 +33,7 @@ type Client struct { PlatformType MeshPlatformTypeClient } -func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string) Client { +func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret, apiToken string) Client { httpClient := &internal.HttpClient{ Client: http.Client{Timeout: 5 * time.Minute}, RootUrl: rootUrl, @@ -40,6 +44,18 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str ApiKey: apiKey, ApiSecret: apiSecret, } + + if apiToken != "" { + httpClient.Authorization = "Bearer " + apiToken + + if expiresAt, err := parseTokenExpiration(apiToken); err == nil { + httpClient.AuthorizationExpiresAt = expiresAt + } else { + // If token has no expiration we assume it is valid for the default duration. + httpClient.AuthorizationExpiresAt = time.Now().Add(6 * time.Hour) + } + } + return Client{ newBuildingBlockClient(ctx, httpClient), newBuildingBlockV2Client(ctx, httpClient), @@ -60,3 +76,28 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str newPlatformTypeClient(ctx, httpClient), } } + +func parseTokenExpiration(token string) (time.Time, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return time.Time{}, fmt.Errorf("invalid token format") + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return time.Time{}, err + } + + var claims struct { + Exp int64 `json:"exp"` + } + if err := json.Unmarshal(payload, &claims); err != nil { + return time.Time{}, err + } + + if claims.Exp == 0 { + return time.Time{}, fmt.Errorf("expiration claim missing") + } + + return time.Unix(claims.Exp, 0), nil +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..0fed152 --- /dev/null +++ b/client_test.go @@ -0,0 +1,69 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseTokenExpiration(t *testing.T) { + // Helper to create a dummy JWT with a specific expiration time + createToken := func(expTime time.Time) string { + header := `{"alg":"HS256","typ":"JWT"}` + payload := map[string]any{ + "sub": "1234567890", + "name": "John Doe", + "exp": expTime.Unix(), + } + + payloadBytes, _ := json.Marshal(payload) + + encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(header)) + encodedPayload := base64.RawURLEncoding.EncodeToString(payloadBytes) + signature := "dummy_signature" + + return fmt.Sprintf("%s.%s.%s", encodedHeader, encodedPayload, signature) + } + + fixedBaseTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) + + t.Run("Valid token", func(t *testing.T) { + expTime := fixedBaseTime + token := createToken(expTime) + + parsedTime, err := parseTokenExpiration(token) + + require.NoError(t, err) + assert.Equal(t, expTime.Unix(), parsedTime.Unix()) + }) + + t.Run("Invalid format - not enough parts", func(t *testing.T) { + token := "invalid.token" + _, err := parseTokenExpiration(token) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid token format") + }) + + t.Run("Invalid Base64 payload", func(t *testing.T) { + token := "header.invalid_base64$.signature" + _, err := parseTokenExpiration(token) + assert.Error(t, err) + }) + + t.Run("Missing exp claim", func(t *testing.T) { + header := `{"alg":"HS256","typ":"JWT"}` + payload := `{"sub":"1234567890"}` // No exp + encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(header)) + encodedPayload := base64.RawURLEncoding.EncodeToString([]byte(payload)) + token := fmt.Sprintf("%s.%s.sig", encodedHeader, encodedPayload) + + _, err := parseTokenExpiration(token) + require.Error(t, err) + assert.Contains(t, err.Error(), "expiration claim missing") + }) +} From cd5eafa8f412595d28584c6718444ac16b307dd5 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 21 Jan 2026 09:16:23 +0100 Subject: [PATCH 086/200] feat: check meshStack version --- client.go | 28 ++++++++++++++++++++++++++-- internal/http_client.go | 9 +++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 3d48749..733cc82 100644 --- a/client.go +++ b/client.go @@ -10,9 +10,13 @@ import ( "strings" "time" + "github.com/hashicorp/go-version" + "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) +const MinMeshStackVersion = "2026.2.0" + type Client struct { BuildingBlock MeshBuildingBlockClient BuildingBlockV2 MeshBuildingBlockV2Client @@ -33,7 +37,7 @@ type Client struct { PlatformType MeshPlatformTypeClient } -func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret, apiToken string) Client { +func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string, apiToken string) (Client, error) { httpClient := &internal.HttpClient{ Client: http.Client{Timeout: 5 * time.Minute}, RootUrl: rootUrl, @@ -56,6 +60,26 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret, ap } } + // Validate meshStack version compatibility + meshInfo, err := httpClient.GetMeshInfo(ctx) + if err != nil { + return Client{}, fmt.Errorf("failed to retrieve meshStack version information from /mesh/info endpoint: %w", err) + } + + minVersion, err := version.NewVersion(MinMeshStackVersion) + if err != nil { + return Client{}, fmt.Errorf("invalid minimum version format %s: %w", MinMeshStackVersion, err) + } + + actualVersion, err := version.NewVersion(meshInfo.Version) + if err != nil { + return Client{}, fmt.Errorf("invalid meshStack version format %s: %w", meshInfo.Version, err) + } + + if actualVersion.LessThan(minVersion) { + return Client{}, fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) + } + return Client{ newBuildingBlockClient(ctx, httpClient), newBuildingBlockV2Client(ctx, httpClient), @@ -74,7 +98,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret, ap newWorkspaceGroupBindingClient(ctx, httpClient), newWorkspaceUserBindingClient(ctx, httpClient), newPlatformTypeClient(ctx, httpClient), - } + }, nil } func parseTokenExpiration(token string) (time.Time, error) { diff --git a/internal/http_client.go b/internal/http_client.go index 80a3d72..ad006d7 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -110,3 +110,12 @@ func unmarshalBody[T any](body []byte, err error) (*T, error) { } return &target, nil } + +type MeshInfo struct { + Version string `json:"version"` +} + +func (c *HttpClient) GetMeshInfo(ctx context.Context) (*MeshInfo, error) { + meshInfoUrl := c.RootUrl.JoinPath("/mesh/info") + return unmarshalBody[MeshInfo](c.doRequest(ctx, "GET", meshInfoUrl)) +} From 0f1e0562384ba330d3e42bf560aaa28514b32581 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 23 Jan 2026 13:48:11 +0100 Subject: [PATCH 087/200] feat: add client/version package for parsing meshStack version limited semver format is supported --- version/version.go | 75 ++++++++++++++++++++++++++++++++ version/version_test.go | 94 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 version/version.go create mode 100644 version/version_test.go diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..5a25958 --- /dev/null +++ b/version/version.go @@ -0,0 +1,75 @@ +package version + +import ( + "cmp" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" +) + +type Version struct { + Major, Minor, Patch int +} + +func Parse(s string) (Version, error) { + parts := strings.Split(s, ".") + if len(parts) != 3 { + return Version{}, fmt.Errorf("cannot parse '%s' as version: expected 3, got %d fields separated by '.'", s, len(parts)) + } + var errs []error + partTo := func(i int, target *int) { + parsed, err := strconv.Atoi(parts[i]) + if err == nil && parsed < 0 { + err = fmt.Errorf("negative number '%d' not allowed", parsed) + } + if err != nil { + errs = append(errs, fmt.Errorf("part i=%d: %w", i, err)) + } else { + *target = parsed + } + } + var result Version + partTo(0, &result.Major) + partTo(1, &result.Minor) + partTo(2, &result.Patch) + if len(errs) > 0 { + return Version{}, fmt.Errorf("cannot parse '%s' as version: %w", s, errors.Join(errs...)) + } + return result, nil +} + +func MustParse(s string) Version { + version, err := Parse(s) + if err != nil { + panic(err) + } + return version +} + +func (v Version) Compare(other Version) int { + if major := cmp.Compare(v.Major, other.Major); major != 0 { + return major + } else if minor := cmp.Compare(v.Minor, other.Minor); minor != 0 { + return minor + } + return cmp.Compare(v.Patch, other.Patch) +} + +func (v Version) Less(other Version) bool { + return v.Compare(other) < 0 +} + +func (v Version) String() string { + return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +func (v *Version) UnmarshalJSON(bytes []byte) (err error) { + var s string + if err = json.Unmarshal(bytes, &s); err != nil { + return + } + *v, err = Parse(s) + return +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 0000000..9e06840 --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,94 @@ +package version + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + assertErrorContainsAllOf := func(contains ...string) assert.ErrorAssertionFunc { + return func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + assert.NotEmpty(t, contains) + allOk := true + for _, contain := range contains { + ok := assert.ErrorContains(t, err, contain, msgAndArgs...) + allOk = allOk && ok + } + return allOk + } + } + tests := []struct { + name string + s string + want Version + wantErr assert.ErrorAssertionFunc + }{ + {"valid 1.0.0", "1.0.0", Version{1, 0, 0}, assert.NoError}, + {"valid 1.3.2", "1.3.2", Version{1, 3, 2}, assert.NoError}, + {"not enough parts", "1.1", Version{}, assertErrorContainsAllOf("cannot parse '1.1' as version: expected 3, got 2 fields separated by '.'")}, + {"negative minor", "1.-1.0", Version{}, assertErrorContainsAllOf("cannot parse '1.-1.0' as version: part i=1: negative number '-1' not allowed")}, + {"not a number", "1.1.x", Version{}, assertErrorContainsAllOf(`cannot parse '1.1.x' as version: part i=2: strconv.Atoi: parsing "x": invalid syntax`)}, + {"number too large", "100000000000000000000.1.0", Version{}, assertErrorContainsAllOf(`cannot parse '100000000000000000000.1.0' as version: part i=0: strconv.Atoi: parsing "100000000000000000000": value out of range`)}, + {"multiple errors", "y.x.1", Version{}, assertErrorContainsAllOf(`part i=0: strconv.Atoi: parsing "y": invalid syntax`, `part i=1: strconv.Atoi: parsing "x": invalid syntax`)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotV, err := Parse(tt.s) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.s)) { + return + } + assert.Equalf(t, tt.want, gotV, "Parse(%v)", tt.s) + }) + } +} + +func TestMustParse(t *testing.T) { + assert.NotPanics(t, func() { + MustParse("1.0.0") + }) + assert.Panics(t, func() { + MustParse("1.x.0") + }) +} + +func TestVersion_Compare(t *testing.T) { + tests := []struct { + v, other string + want int + }{ + {"0.0.0", "0.0.0", 0}, + {"0.1.0", "0.1.0", 0}, + {"1.1.0", "0.1.0", 1}, + {"1.1.12312331222", "2.1.0", -1}, + {"1.2.0", "1.3.0", -1}, + {"1.2.1", "1.2.0", 1}, + } + for _, tt := range tests { + symbol := "==" + if tt.want < 0 { + symbol = "<" + } else if tt.want > 0 { + symbol = ">" + } + t.Run(fmt.Sprintf("%s %s %s", tt.v, symbol, tt.other), func(t *testing.T) { + v, err := Parse(tt.v) + require.NoError(t, err) + other, err := Parse(tt.other) + require.NoError(t, err) + cmp := v.Compare(other) + assert.Equal(t, tt.want, cmp) + if cmp < 0 { + assert.True(t, v.Less(other)) + } else { + assert.False(t, v.Less(other)) + } + }) + } +} + +func TestVersion_String(t *testing.T) { + assert.Equal(t, "1.2.3", Version{1, 2, 3}.String()) +} From afc3e89ba796395a62e75494d2dcf8ff9195cbe4 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 23 Jan 2026 13:52:21 +0100 Subject: [PATCH 088/200] fix: use client/version --- client.go | 24 +++++------------------- internal/http_client.go | 4 +++- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/client.go b/client.go index 733cc82..2b0e818 100644 --- a/client.go +++ b/client.go @@ -10,12 +10,11 @@ import ( "strings" "time" - "github.com/hashicorp/go-version" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -const MinMeshStackVersion = "2026.2.0" +var MinMeshStackVersion = version.MustParse("2026.2.0") type Client struct { BuildingBlock MeshBuildingBlockClient @@ -60,23 +59,10 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str } } - // Validate meshStack version compatibility - meshInfo, err := httpClient.GetMeshInfo(ctx) - if err != nil { + // Check meshStack version compatibility + if meshInfo, err := httpClient.GetMeshInfo(ctx); err != nil { return Client{}, fmt.Errorf("failed to retrieve meshStack version information from /mesh/info endpoint: %w", err) - } - - minVersion, err := version.NewVersion(MinMeshStackVersion) - if err != nil { - return Client{}, fmt.Errorf("invalid minimum version format %s: %w", MinMeshStackVersion, err) - } - - actualVersion, err := version.NewVersion(meshInfo.Version) - if err != nil { - return Client{}, fmt.Errorf("invalid meshStack version format %s: %w", meshInfo.Version, err) - } - - if actualVersion.LessThan(minVersion) { + } else if meshInfo.Version.Less(MinMeshStackVersion) { return Client{}, fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) } diff --git a/internal/http_client.go b/internal/http_client.go index ad006d7..0fee9d5 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -11,6 +11,8 @@ import ( "net/url" "slices" "time" + + "github.com/meshcloud/terraform-provider-meshstack/client/version" ) var ( @@ -112,7 +114,7 @@ func unmarshalBody[T any](body []byte, err error) (*T, error) { } type MeshInfo struct { - Version string `json:"version"` + Version version.Version `json:"version"` } func (c *HttpClient) GetMeshInfo(ctx context.Context) (*MeshInfo, error) { From 8752fc26403c9baf0acc5e4282c563eb3bf37a81 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 28 Jan 2026 10:16:31 +0100 Subject: [PATCH 089/200] fix: subscription creation error cooldown nullable --- platform_config_azure.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform_config_azure.go b/platform_config_azure.go index 2d0375d..5fc6b81 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -49,7 +49,7 @@ type AzureEnterpriseEnrollmentConfig struct { EnrollmentAccountId string `json:"enrollmentAccountId" tfsdk:"enrollment_account_id"` SubscriptionOfferType string `json:"subscriptionOfferType" tfsdk:"subscription_offer_type"` UseLegacySubscriptionEnrollment bool `json:"useLegacySubscriptionEnrollment" tfsdk:"use_legacy_subscription_enrollment"` - SubscriptionCreationErrorCooldownSec int64 `json:"subscriptionCreationErrorCooldownSec" tfsdk:"subscription_creation_error_cooldown_sec"` + SubscriptionCreationErrorCooldownSec *int64 `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` } type AzureCustomerAgreementConfig struct { @@ -57,7 +57,7 @@ type AzureCustomerAgreementConfig struct { DestinationEntraId string `json:"destinationEntraId" tfsdk:"destination_entra_id"` SourceEntraTenant string `json:"sourceEntraTenant" tfsdk:"source_entra_tenant"` BillingScope string `json:"billingScope" tfsdk:"billing_scope"` - SubscriptionCreationErrorCooldownSec int64 `json:"subscriptionCreationErrorCooldownSec" tfsdk:"subscription_creation_error_cooldown_sec"` + SubscriptionCreationErrorCooldownSec *int64 `json:"subscriptionCreationErrorCooldownSec,omitempty" tfsdk:"subscription_creation_error_cooldown_sec"` } type AzurePreProvisionedSubscriptionConfig struct { From 9f43b5e52f5ab2ae2203c5a7cb83ad461d7002af Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 26 Jan 2026 17:36:00 +0100 Subject: [PATCH 090/200] feat: add `owned_by_workspace` for platform_type CU-86c7m8w3m --- platform_type.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/platform_type.go b/platform_type.go index 366cb19..ec2c317 100644 --- a/platform_type.go +++ b/platform_type.go @@ -23,9 +23,10 @@ type MeshPlatformTypeLifecycle struct { } type MeshPlatformTypeMetadata struct { - Name string `json:"name" tfsdk:"name"` - CreatedOn *string `json:"createdOn" tfsdk:"created_on"` - Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn *string `json:"createdOn" tfsdk:"created_on"` + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` } type MeshPlatformTypeSpec struct { @@ -43,7 +44,8 @@ type MeshPlatformTypeCreate struct { } type MeshPlatformTypeCreateMetadata struct { - Name string `json:"name" tfsdk:"name"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } type MeshPlatformTypeClient struct { From 2595875957b1cd426552398914fc8d27ad53a41d Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Wed, 4 Feb 2026 13:13:58 +0100 Subject: [PATCH 091/200] feat: custom platforms in meshstack_platform --- platform.go | 1 + platform_config_custom.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 platform_config_custom.go diff --git a/platform.go b/platform.go index bd7b7fc..aa92991 100644 --- a/platform.go +++ b/platform.go @@ -62,6 +62,7 @@ type PlatformAvailability struct { type PlatformConfig struct { Type string `json:"type" tfsdk:"type"` + Custom *CustomPlatformConfig `json:"custom,omitempty" tfsdk:"custom"` Aws *AwsPlatformConfig `json:"aws,omitempty" tfsdk:"aws"` Aks *AksPlatformConfig `json:"aks,omitempty" tfsdk:"aks"` Azure *AzurePlatformConfig `json:"azure,omitempty" tfsdk:"azure"` diff --git a/platform_config_custom.go b/platform_config_custom.go new file mode 100644 index 0000000..293cc39 --- /dev/null +++ b/platform_config_custom.go @@ -0,0 +1,15 @@ +package client + +type CustomPlatformConfig struct { + PlatformTypeRef PlatformTypeRef `json:"platformTypeRef" tfsdk:"platform_type_ref"` + Metering *CustomMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` +} + +type PlatformTypeRef struct { + Name string `json:"name" tfsdk:"name"` + Kind string `json:"kind" tfsdk:"kind"` +} + +type CustomMeteringConfig struct { + Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` +} From ae81c68690d0b2e0663f2a4e5d28e1f37d383efb Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 6 Feb 2026 15:52:34 +0100 Subject: [PATCH 092/200] feat: service instance client --- client.go | 2 ++ service_instance.go | 70 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 service_instance.go diff --git a/client.go b/client.go index 2b0e818..f55c598 100644 --- a/client.go +++ b/client.go @@ -27,6 +27,7 @@ type Client struct { Project MeshProjectClient ProjectGroupBinding MeshProjectGroupBindingClient ProjectUserBinding MeshProjectUserBindingClient + ServiceInstance MeshServiceInstanceClient TagDefinition MeshTagDefinitionClient Tenant MeshTenantClient TenantV4 MeshTenantV4Client @@ -77,6 +78,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str newProjectClient(ctx, httpClient), newProjectGroupBindingClient(ctx, httpClient), newProjectUserBindingClient(ctx, httpClient), + newServiceInstanceClient(ctx, httpClient), newTagDefinitionClient(ctx, httpClient), newTenantClient(ctx, httpClient), newTenantV4Client(ctx, httpClient), diff --git a/service_instance.go b/service_instance.go new file mode 100644 index 0000000..58b6f2f --- /dev/null +++ b/service_instance.go @@ -0,0 +1,70 @@ +package client + +import ( + "context" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + +type MeshServiceInstance struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshServiceInstanceMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshServiceInstanceSpec `json:"spec" tfsdk:"spec"` +} + +type MeshServiceInstanceMetadata struct { + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + MarketplaceIdentifier string `json:"marketplaceIdentifier" tfsdk:"marketplace_identifier"` + InstanceId string `json:"instanceId" tfsdk:"instance_id"` +} + +type MeshServiceInstanceSpec struct { + Creator string `json:"creator" tfsdk:"creator"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + PlanId string `json:"planId" tfsdk:"plan_id"` + ServiceId string `json:"serviceId" tfsdk:"service_id"` +} + +type MeshServiceInstanceClient struct { + meshObject internal.MeshObjectClient[MeshServiceInstance] +} + +type MeshServiceInstanceFilter struct { + WorkspaceIdentifier *string + ProjectIdentifier *string + MarketplaceIdentifier *string + ServiceIdentifier *string + PlanIdentifier *string +} + +func newServiceInstanceClient(ctx context.Context, httpClient *internal.HttpClient) MeshServiceInstanceClient { + return MeshServiceInstanceClient{internal.NewMeshObjectClient[MeshServiceInstance](ctx, httpClient, "v2")} +} + +func (c MeshServiceInstanceClient) Read(ctx context.Context, instanceId string) (*MeshServiceInstance, error) { + return c.meshObject.Get(ctx, instanceId) +} + +func (c MeshServiceInstanceClient) List(ctx context.Context, filter *MeshServiceInstanceFilter) ([]MeshServiceInstance, error) { + var options []internal.RequestOption + if filter != nil { + if filter.WorkspaceIdentifier != nil { + options = append(options, internal.WithUrlQuery("workspaceIdentifier", *filter.WorkspaceIdentifier)) + } + if filter.ProjectIdentifier != nil { + options = append(options, internal.WithUrlQuery("projectIdentifier", *filter.ProjectIdentifier)) + } + if filter.MarketplaceIdentifier != nil { + options = append(options, internal.WithUrlQuery("marketplaceIdentifier", *filter.MarketplaceIdentifier)) + } + if filter.ServiceIdentifier != nil { + options = append(options, internal.WithUrlQuery("serviceIdentifier", *filter.ServiceIdentifier)) + } + if filter.PlanIdentifier != nil { + options = append(options, internal.WithUrlQuery("planIdentifier", *filter.PlanIdentifier)) + } + } + return c.meshObject.List(ctx, options...) +} From 745f0b9b0bd108ffdf7524b4b1b9fc2e7c3b1a39 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 12 Feb 2026 15:42:53 +0100 Subject: [PATCH 093/200] feat: add support for `custom` landing zone CU-86c68mqgp --- landingzone.go | 1 + platform_properties_custom.go | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 platform_properties_custom.go diff --git a/landingzone.go b/landingzone.go index 6588d19..0deb06d 100644 --- a/landingzone.go +++ b/landingzone.go @@ -49,6 +49,7 @@ type MeshLandingZonePlatformProperties struct { Aks *AksPlatformProperties `json:"aks" tfsdk:"aks"` Azure *AzurePlatformProperties `json:"azure" tfsdk:"azure"` AzureRg *AzureRgPlatformProperties `json:"azurerg" tfsdk:"azurerg"` + Custom *CustomPlatformProperties `json:"custom" tfsdk:"custom"` Gcp *GcpPlatformProperties `json:"gcp" tfsdk:"gcp"` Kubernetes *KubernetesPlatformProperties `json:"kubernetes" tfsdk:"kubernetes"` OpenShift *OpenShiftPlatformProperties `json:"openshift" tfsdk:"openshift"` diff --git a/platform_properties_custom.go b/platform_properties_custom.go new file mode 100644 index 0000000..0d721af --- /dev/null +++ b/platform_properties_custom.go @@ -0,0 +1,5 @@ +package client + +type CustomPlatformProperties struct { + // Intentionally left empty, as custom platforms do not have any properties. +} From 03e67dcc80732a979734303a330573925d70731b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 15 Jan 2026 14:22:30 +0100 Subject: [PATCH 094/200] refactor: rename name to better term 'kind' in internal.MeshObjectClient --- internal/mesh_object_client.go | 42 +++++++++++++++-------------- internal/mesh_object_client_test.go | 22 +++++++-------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 96f4312..7957654 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -21,30 +21,32 @@ import ( // which are embedded in HttpClient for convenient construction with NewMeshObjectClient. type MeshObjectClient[M any] struct { *HttpClient - Name string + Kind string ApiVersion string ApiUrl *url.URL } // NewMeshObjectClient creates a new [MeshObjectClient] for a specific meshObject type with automatic URL path inference. -// The meshObject name is inferred from type M, and the API URL is constructed from explicitApiPaths or the pluralized type name. -func NewMeshObjectClient[M any](ctx context.Context, httpClient *HttpClient, apiVersion string, explicitApiPaths ...string) MeshObjectClient[M] { - name, typeName := inferMeshObjectName[M]() - - if len(explicitApiPaths) == 0 { - explicitApiPaths = []string{strings.ToLower(pluralizeName(name))} +// The meshObject kind is inferred from type M. T +// The API URL is constructed from explicitApiPathElems if provided, +// otherwise the pluralized and lowercased kind is used as a single element. +func NewMeshObjectClient[M any](ctx context.Context, httpClient *HttpClient, apiVersion string, explicitApiPathElems ...string) MeshObjectClient[M] { + kind, typeName := inferMeshObjectKindFromType[M]() + + if len(explicitApiPathElems) == 0 { + explicitApiPathElems = []string{strings.ToLower(pluralizeKind(kind))} } - explicitApiPaths = slices.Insert(explicitApiPaths, 0, "/api/meshobjects") - apiUrl := httpClient.RootUrl.JoinPath(explicitApiPaths...) - Log.Info(ctx, fmt.Sprintf("initialized %s", typeName), "url", apiUrl.String(), "name", name, "version", apiVersion) - return MeshObjectClient[M]{httpClient, name, apiVersion, apiUrl} + explicitApiPathElems = slices.Insert(explicitApiPathElems, 0, "/api/meshobjects") + apiUrl := httpClient.RootUrl.JoinPath(explicitApiPathElems...) + Log.Info(ctx, fmt.Sprintf("initialized %s client", typeName), "url", apiUrl.String(), "kind", kind, "version", apiVersion) + return MeshObjectClient[M]{httpClient, kind, apiVersion, apiUrl} } -func inferMeshObjectName[M any]() (name, typeName string) { +func inferMeshObjectKindFromType[M any]() (lowercase, typeName string) { var zero M typeName = reflect.TypeOf(zero).Name() - name = lowercaseFirst(typeName) - return regexp.MustCompile(`V\d+$`).ReplaceAllString(name, ""), typeName + lowercase = lowercaseFirst(typeName) + return regexp.MustCompile(`V\d+$`).ReplaceAllString(lowercase, ""), typeName } func lowercaseFirst(s string) string { @@ -56,16 +58,16 @@ func lowercaseFirst(s string) string { return string(runes) } -func pluralizeName(name string) string { - if strings.HasSuffix(name, "y") { +func pluralizeKind(kind string) string { + if strings.HasSuffix(kind, "y") { // this is ok, as we don't have meshObjects ending in 'y' yet, so take this shortcut - panic(fmt.Sprintf("Correctly pluralizing '%s' is not supported yet", name)) + panic(fmt.Sprintf("Correctly pluralizing meshObject kind '%s' is not supported yet", kind)) } - return fmt.Sprintf("%ss", name) + return fmt.Sprintf("%ss", kind) } func (c MeshObjectClient[M]) meshObjectMimeType() string { - return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", c.Name, c.ApiVersion) + return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", c.Kind, c.ApiVersion) } // Get retrieves a meshObject by ID. Returns nil if not found. @@ -97,7 +99,7 @@ func (c MeshObjectClient[M]) Delete(ctx context.Context, id string) (err error) // Accepts optional [RequestOption] parameters for filtering and querying. func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) ([]M, error) { var result []M - embeddedKey := pluralizeName(c.Name) + embeddedKey := pluralizeKind(c.Kind) pageNumber := 0 for { diff --git a/internal/mesh_object_client_test.go b/internal/mesh_object_client_test.go index 4c9219c..fb8f068 100644 --- a/internal/mesh_object_client_test.go +++ b/internal/mesh_object_client_test.go @@ -11,36 +11,36 @@ type MeshBuildingBlockV2 struct{} type MeshTenantV4 struct{} type MeshWorkspace struct{} -func TestInferMeshObjectName(t *testing.T) { +func Test_inferMeshObjectKindFromType(t *testing.T) { tests := []struct { - name string + kind string testFunc func() (string, string) expected string }{ { - name: "MeshBuildingBlock", - testFunc: inferMeshObjectName[MeshBuildingBlock], + kind: "MeshBuildingBlock", + testFunc: inferMeshObjectKindFromType[MeshBuildingBlock], expected: "meshBuildingBlock", }, { - name: "MeshBuildingBlockV2", - testFunc: inferMeshObjectName[MeshBuildingBlockV2], + kind: "MeshBuildingBlockV2", + testFunc: inferMeshObjectKindFromType[MeshBuildingBlockV2], expected: "meshBuildingBlock", }, { - name: "MeshWorkspace", - testFunc: inferMeshObjectName[MeshWorkspace], + kind: "MeshWorkspace", + testFunc: inferMeshObjectKindFromType[MeshWorkspace], expected: "meshWorkspace", }, { - name: "MeshTenantV4", - testFunc: inferMeshObjectName[MeshTenantV4], + kind: "MeshTenantV4", + testFunc: inferMeshObjectKindFromType[MeshTenantV4], expected: "meshTenant", }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.kind, func(t *testing.T) { actual, _ := tt.testFunc() assert.Equal(t, tt.expected, actual) }) From cb8d5963e275c6d6b056a8a5c84ea52322cb6bf5 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 21 Jan 2026 14:34:51 +0100 Subject: [PATCH 095/200] refactor: move Secret DTO from platform to commonly shared client/types package (as Secret) --- platform.go | 5 ----- platform_config_aks.go | 4 +++- platform_config_aws.go | 8 +++++--- platform_config_azure.go | 6 ++++-- platform_config_gcp.go | 4 +++- platform_config_kubernetes.go | 4 +++- types/clienttypes.go | 12 ++++++++++++ 7 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 types/clienttypes.go diff --git a/platform.go b/platform.go index aa92991..a0b0e90 100644 --- a/platform.go +++ b/platform.go @@ -34,11 +34,6 @@ type MeshPlatformSpec struct { QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` } -type SecretEmbedded struct { - Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` - // TODO: add Hash field -} - type QuotaDefinition struct { QuotaKey string `json:"quotaKey" tfsdk:"quota_key"` MinValue int `json:"minValue" tfsdk:"min_value"` diff --git a/platform_config_aks.go b/platform_config_aks.go index 369650d..8521a30 100644 --- a/platform_config_aks.go +++ b/platform_config_aks.go @@ -1,5 +1,7 @@ package client +import "github.com/meshcloud/terraform-provider-meshstack/client/types" + type AksPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` @@ -8,7 +10,7 @@ type AksPlatformConfig struct { } type AksReplicationConfig struct { - AccessToken SecretEmbedded `json:"accessToken" tfsdk:"access_token"` + AccessToken types.Secret `json:"accessToken" tfsdk:"access_token"` NamespaceNamePattern string `json:"namespaceNamePattern" tfsdk:"namespace_name_pattern"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` ServicePrincipal AksServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` diff --git a/platform_config_aws.go b/platform_config_aws.go index 01805e6..7bfe596 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -1,5 +1,7 @@ package client +import "github.com/meshcloud/terraform-provider-meshstack/client/types" + type AwsPlatformConfig struct { Region string `json:"region,omitempty" tfsdk:"region"` Replication *AwsReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` @@ -36,8 +38,8 @@ type AwsAuth struct { } type AwsServiceUserCredential struct { - AccessKey string `json:"accessKey" tfsdk:"access_key"` - SecretKey SecretEmbedded `json:"secretKey" tfsdk:"secret_key"` + AccessKey string `json:"accessKey" tfsdk:"access_key"` + SecretKey types.Secret `json:"secretKey" tfsdk:"secret_key"` } type AwsWorkloadIdentityCredential struct { @@ -48,7 +50,7 @@ type AwsSsoConfig struct { ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` Arn string `json:"arn" tfsdk:"arn"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - SsoAccessToken SecretEmbedded `json:"ssoAccessToken" tfsdk:"sso_access_token"` + SsoAccessToken types.Secret `json:"ssoAccessToken" tfsdk:"sso_access_token"` AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` } diff --git a/platform_config_azure.go b/platform_config_azure.go index 5fc6b81..c805a2e 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -1,5 +1,7 @@ package client +import "github.com/meshcloud/terraform-provider-meshstack/client/types" + type AzurePlatformConfig struct { EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` Replication *AzureReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` @@ -29,8 +31,8 @@ type AzureServicePrincipalConfig struct { } type AzureAuthConfig struct { - Type string `json:"type" tfsdk:"type"` - Credential *SecretEmbedded `json:"credential,omitempty" tfsdk:"credential"` + Type string `json:"type" tfsdk:"type"` + Credential *types.Secret `json:"credential,omitempty" tfsdk:"credential"` } type AzureGraphApiCredentials struct { diff --git a/platform_config_gcp.go b/platform_config_gcp.go index 83cc037..8febcb6 100644 --- a/platform_config_gcp.go +++ b/platform_config_gcp.go @@ -1,5 +1,7 @@ package client +import "github.com/meshcloud/terraform-provider-meshstack/client/types" + type GcpPlatformConfig struct { Replication *GcpReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` Metering *GcpMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` @@ -23,7 +25,7 @@ type GcpReplicationConfig struct { type GcpServiceAccountConfig struct { Type string `json:"type" tfsdk:"type"` - Credential *SecretEmbedded `json:"credential,omitempty" tfsdk:"credential"` + Credential *types.Secret `json:"credential,omitempty" tfsdk:"credential"` WorkloadIdentity *GcpServiceAccountWorkloadIdentityConfig `json:"workloadIdentity,omitempty" tfsdk:"workload_identity"` } diff --git a/platform_config_kubernetes.go b/platform_config_kubernetes.go index 893ba2d..e4b34cf 100644 --- a/platform_config_kubernetes.go +++ b/platform_config_kubernetes.go @@ -1,5 +1,7 @@ package client +import "github.com/meshcloud/terraform-provider-meshstack/client/types" + type KubernetesPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` @@ -13,7 +15,7 @@ type KubernetesReplicationConfig struct { } type KubernetesClientConfig struct { - AccessToken SecretEmbedded `json:"accessToken" tfsdk:"access_token"` + AccessToken types.Secret `json:"accessToken" tfsdk:"access_token"` } type KubernetesMeteringConfig struct { diff --git a/types/clienttypes.go b/types/clienttypes.go new file mode 100644 index 0000000..08d597d --- /dev/null +++ b/types/clienttypes.go @@ -0,0 +1,12 @@ +package types + +type ( + String = string + Number = int64 + Any = any + + Secret struct { + Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` + Hash *string `json:"hash,omitempty" tfsdk:"-"` + } +) From d034bcb3af66e103485ac27f68ac8f416c42f3b5 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 21 Jan 2026 14:35:18 +0100 Subject: [PATCH 096/200] feat: add ptr.To helper in client/types/ptr --- types/ptr/pointer.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 types/ptr/pointer.go diff --git a/types/ptr/pointer.go b/types/ptr/pointer.go new file mode 100644 index 0000000..6c3ee9b --- /dev/null +++ b/types/ptr/pointer.go @@ -0,0 +1,5 @@ +package ptr + +func To[T any](v T) *T { + return &v +} From 758df6c86b492d9914929a0ced02451386beb11a Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 30 Jan 2026 11:10:23 +0100 Subject: [PATCH 097/200] feat: support defining Go enum strings --- types/enum/enum.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 types/enum/enum.go diff --git a/types/enum/enum.go b/types/enum/enum.go new file mode 100644 index 0000000..4934aa7 --- /dev/null +++ b/types/enum/enum.go @@ -0,0 +1,53 @@ +package enum + +import ( + "fmt" + "strings" + + "github.com/meshcloud/terraform-provider-meshstack/client/types/ptr" +) + +func Of[T ~string](entries ...Entry[T]) Enum[T] { + return entries +} + +type Enum[T ~string] []Entry[T] + +func (e *Enum[T]) Entry(v string) (ee Entry[T]) { + ee = Entry[T](v) + *e = append(*e, ee) + return +} + +func (e Enum[T]) to(mapper func(entry Entry[T]) string) (result []string) { + for _, ee := range e { + result = append(result, mapper(ee)) + } + return +} + +func (e Enum[T]) Strings() []string { + return e.to(Entry[T].String) +} + +func (e Enum[T]) Markdown() string { + return strings.Join(e.to(Entry[T].Markdown), ", ") +} + +type Entry[T ~string] string + +func (ee Entry[T]) Ptr() *T { + return ptr.To(ee.Unwrap()) +} + +func (ee Entry[T]) Unwrap() T { + return T(ee) +} + +func (ee Entry[T]) String() string { + return string(ee) +} + +func (ee Entry[T]) Markdown() string { + return fmt.Sprintf("`%s`", ee) +} From 1fd17bc2c176a0efafa67cfdc314a696dfcca68d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 30 Jan 2026 11:08:09 +0100 Subject: [PATCH 098/200] feat: add Variant[X, Y] in client/types/variant --- types/variant/variant.go | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 types/variant/variant.go diff --git a/types/variant/variant.go b/types/variant/variant.go new file mode 100644 index 0000000..6795181 --- /dev/null +++ b/types/variant/variant.go @@ -0,0 +1,85 @@ +package variant + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" +) + +// A Variant represents a single JSON map entry having two different Go type representations X and Y. +// After JSON unmarshalling you can check with HasX, HasY which field has been detected, while X is preferred. +// An example usage is a Client DTO response which can either be struct representing a secret hash, +// or a simple string response if that's a non-sensitive value. +type Variant[X, Y any] struct { + X X + Y Y +} + +var ( + _ json.Unmarshaler = (*Variant[int, string])(nil) + _ json.Marshaler = Variant[int, string]{} +) + +func (v Variant[X, Y]) MarshalJSON() ([]byte, error) { + if v.HasX() { + return json.Marshal(v.X) + } else if v.HasY() { + return json.Marshal(v.Y) + } else { + return json.Marshal(nil) + } +} + +func (v Variant[X, Y]) HasX() bool { + x := reflect.ValueOf(v.X) + return x.IsValid() && !x.IsZero() +} + +func (v Variant[X, Y]) HasY() bool { + y := reflect.ValueOf(v.Y) + return y.IsValid() && !y.IsZero() +} + +func (v Variant[X, Y]) WithX(action func(x *X)) { + if v.HasX() { + action(&v.X) + } else { + action(nil) + } +} + +func (v Variant[X, Y]) WithY(action func(y *Y)) { + if v.HasY() { + action(&v.Y) + } else { + action(nil) + } +} + +func (v *Variant[X, Y]) UnmarshalJSON(bytes []byte) error { + errX := json.Unmarshal(bytes, &v.X) + errY := json.Unmarshal(bytes, &v.Y) + switch { + case v.HasX() && v.HasY(): + // Explicitly prefer X over Y and set Y to zero even if unmarshalling has also worked, + // this supports having Y with catch-all type 'any' + var zeroY Y + v.Y = zeroY + return errX + case v.HasX(): + return errX + case v.HasY(): + return errY + default: + var nothing any + if err := json.Unmarshal(bytes, ¬hing); err != nil { + return fmt.Errorf("cannot unmarshal to any: %w", err) + } + if nothing == nil { + // support optional unmarshalling aka neither X nor Y is set + return nil + } + return errors.Join(fmt.Errorf("variant[%T, %T]: cannot unmarshal '%s' to any field", v.X, v.Y, string(bytes)), errX, errY) + } +} From 90c3f91e6eea5f06f46f6a0d212c14cbca7c05e3 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 4 Feb 2026 11:07:00 +0100 Subject: [PATCH 099/200] feat: add mock client and resource test for meshstack_tag_definition --- tag_definition.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tag_definition.go b/tag_definition.go index 5b78ca9..dfc6300 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -69,30 +69,38 @@ type TagValueMultiSelect struct { DefaultValue *[]string `json:"defaultValue,omitempty" tfsdk:"default_value"` } -type MeshTagDefinitionClient struct { +type MeshTagDefinitionClient interface { + List(ctx context.Context) ([]MeshTagDefinition, error) + Read(ctx context.Context, name string) (*MeshTagDefinition, error) + Create(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) + Update(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) + Delete(ctx context.Context, name string) error +} + +type meshTagDefinitionClient struct { meshObject internal.MeshObjectClient[MeshTagDefinition] } func newTagDefinitionClient(ctx context.Context, httpClient *internal.HttpClient) MeshTagDefinitionClient { - return MeshTagDefinitionClient{internal.NewMeshObjectClient[MeshTagDefinition](ctx, httpClient, "v1")} + return meshTagDefinitionClient{internal.NewMeshObjectClient[MeshTagDefinition](ctx, httpClient, "v1")} } -func (c MeshTagDefinitionClient) List(ctx context.Context) ([]MeshTagDefinition, error) { +func (c meshTagDefinitionClient) List(ctx context.Context) ([]MeshTagDefinition, error) { return c.meshObject.List(ctx) } -func (c MeshTagDefinitionClient) Read(ctx context.Context, name string) (*MeshTagDefinition, error) { +func (c meshTagDefinitionClient) Read(ctx context.Context, name string) (*MeshTagDefinition, error) { return c.meshObject.Get(ctx, name) } -func (c MeshTagDefinitionClient) Create(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { +func (c meshTagDefinitionClient) Create(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { return c.meshObject.Post(ctx, tagDefinition) } -func (c MeshTagDefinitionClient) Update(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { +func (c meshTagDefinitionClient) Update(ctx context.Context, tagDefinition *MeshTagDefinition) (*MeshTagDefinition, error) { return c.meshObject.Put(ctx, tagDefinition.Metadata.Name, tagDefinition) } -func (c MeshTagDefinitionClient) Delete(ctx context.Context, name string) error { +func (c meshTagDefinitionClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } From e61f057ee879fb9c31fce2bc8c0455ef2bbe693d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 4 Feb 2026 11:35:51 +0100 Subject: [PATCH 100/200] feat: add mock client and resource/datasource test for meshstack_platform, remove created_on, deleted_on --- platform.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/platform.go b/platform.go index a0b0e90..34a7d89 100644 --- a/platform.go +++ b/platform.go @@ -14,11 +14,9 @@ type MeshPlatform struct { } type MeshPlatformMetadata struct { - Name string `json:"name" tfsdk:"name"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - Uuid string `json:"uuid" tfsdk:"uuid"` - CreatedOn string `json:"createdOn" tfsdk:"created_on"` - DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Uuid string `json:"uuid" tfsdk:"uuid"` } type MeshPlatformSpec struct { @@ -105,26 +103,33 @@ type TagMapper struct { ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` } -type MeshPlatformClient struct { +type MeshPlatformClient interface { + Read(ctx context.Context, uuid string) (*MeshPlatform, error) + Create(ctx context.Context, platform *MeshPlatformCreate) (*MeshPlatform, error) + Update(ctx context.Context, uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) + Delete(ctx context.Context, uuid string) error +} + +type meshPlatformClient struct { meshObject internal.MeshObjectClient[MeshPlatform] } func newPlatformClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformClient { - return MeshPlatformClient{internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2-preview")} + return meshPlatformClient{internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2-preview")} } -func (c MeshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatform, error) { +func (c meshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatform, error) { return c.meshObject.Get(ctx, uuid) } -func (c MeshPlatformClient) Create(ctx context.Context, platform *MeshPlatformCreate) (*MeshPlatform, error) { +func (c meshPlatformClient) Create(ctx context.Context, platform *MeshPlatformCreate) (*MeshPlatform, error) { return c.meshObject.Post(ctx, platform) } -func (c MeshPlatformClient) Update(ctx context.Context, uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { +func (c meshPlatformClient) Update(ctx context.Context, uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { return c.meshObject.Put(ctx, uuid, platform) } -func (c MeshPlatformClient) Delete(ctx context.Context, uuid string) error { +func (c meshPlatformClient) Delete(ctx context.Context, uuid string) error { return c.meshObject.Delete(ctx, uuid) } From bac587fc688cd7ea9e6718716f96b5c183e26970 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 4 Feb 2026 11:48:03 +0100 Subject: [PATCH 101/200] feat: add mock client and extend resource test for meshstack_location --- location.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/location.go b/location.go index 3656744..13bbee1 100644 --- a/location.go +++ b/location.go @@ -37,26 +37,33 @@ type MeshLocationCreateMetadata struct { Name string `json:"name" tfsdk:"name"` } -type MeshLocationClient struct { +type MeshLocationClient interface { + Read(ctx context.Context, name string) (*MeshLocation, error) + Create(ctx context.Context, location *MeshLocationCreate) (*MeshLocation, error) + Update(ctx context.Context, name string, location *MeshLocationCreate) (*MeshLocation, error) + Delete(ctx context.Context, name string) error +} + +type meshLocationClient struct { meshObject internal.MeshObjectClient[MeshLocation] } func newLocationClient(ctx context.Context, httpClient *internal.HttpClient) MeshLocationClient { - return MeshLocationClient{internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1-preview")} + return meshLocationClient{internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1-preview")} } -func (c MeshLocationClient) Read(ctx context.Context, name string) (*MeshLocation, error) { +func (c meshLocationClient) Read(ctx context.Context, name string) (*MeshLocation, error) { return c.meshObject.Get(ctx, name) } -func (c MeshLocationClient) Create(ctx context.Context, location *MeshLocationCreate) (*MeshLocation, error) { +func (c meshLocationClient) Create(ctx context.Context, location *MeshLocationCreate) (*MeshLocation, error) { return c.meshObject.Post(ctx, location) } -func (c MeshLocationClient) Update(ctx context.Context, name string, location *MeshLocationCreate) (*MeshLocation, error) { +func (c meshLocationClient) Update(ctx context.Context, name string, location *MeshLocationCreate) (*MeshLocation, error) { return c.meshObject.Put(ctx, name, location) } -func (c MeshLocationClient) Delete(ctx context.Context, name string) error { +func (c meshLocationClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } From ad78a3ee575359576184926df24b0094cdfbd8d8 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 4 Feb 2026 21:47:25 +0100 Subject: [PATCH 102/200] feat: add mock client and resource/datasource test for meshstack_platform_type, fix missing owned_by_workspace in example --- platform_type.go | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/platform_type.go b/platform_type.go index ec2c317..3e93e09 100644 --- a/platform_type.go +++ b/platform_type.go @@ -25,7 +25,6 @@ type MeshPlatformTypeLifecycle struct { type MeshPlatformTypeMetadata struct { Name string `json:"name" tfsdk:"name"` OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - CreatedOn *string `json:"createdOn" tfsdk:"created_on"` Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` } @@ -48,31 +47,39 @@ type MeshPlatformTypeCreateMetadata struct { OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -type MeshPlatformTypeClient struct { +type MeshPlatformTypeClient interface { + Create(ctx context.Context, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) + Read(ctx context.Context, identifier string) (*MeshPlatformType, error) + Update(ctx context.Context, name string, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) + Delete(ctx context.Context, name string) error + List(ctx context.Context, category *string, lifecycleStatus *string) ([]MeshPlatformType, error) +} + +type meshPlatformTypeClient struct { meshObject internal.MeshObjectClient[MeshPlatformType] } func newPlatformTypeClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformTypeClient { - return MeshPlatformTypeClient{internal.NewMeshObjectClient[MeshPlatformType](ctx, httpClient, "v1-preview")} + return meshPlatformTypeClient{internal.NewMeshObjectClient[MeshPlatformType](ctx, httpClient, "v1-preview")} } -func (c MeshPlatformTypeClient) Read(ctx context.Context, identifier string) (*MeshPlatformType, error) { - return c.meshObject.Get(ctx, identifier) +func (c meshPlatformTypeClient) Create(ctx context.Context, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { + return c.meshObject.Post(ctx, platformType) } -func (c MeshPlatformTypeClient) Create(ctx context.Context, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { - return c.meshObject.Post(ctx, platformType) +func (c meshPlatformTypeClient) Read(ctx context.Context, identifier string) (*MeshPlatformType, error) { + return c.meshObject.Get(ctx, identifier) } -func (c MeshPlatformTypeClient) Update(ctx context.Context, name string, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { +func (c meshPlatformTypeClient) Update(ctx context.Context, name string, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { return c.meshObject.Put(ctx, name, platformType) } -func (c MeshPlatformTypeClient) Delete(ctx context.Context, name string) error { +func (c meshPlatformTypeClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } -func (c MeshPlatformTypeClient) List(ctx context.Context, category *string, lifecycleStatus *string) ([]MeshPlatformType, error) { +func (c meshPlatformTypeClient) List(ctx context.Context, category *string, lifecycleStatus *string) ([]MeshPlatformType, error) { var options []internal.RequestOption if category != nil { options = append(options, internal.WithUrlQuery("category", *category)) From 4f99ecfdcd348cc6f9f6c7c765bd3c67c6c66f10 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 5 Feb 2026 15:12:24 +0100 Subject: [PATCH 103/200] feat: implement meshstack_integration resource with mock client --- buildingblock_runner.go | 6 +++ integration.go | 90 ++++++++++++++++++++++++++++++++++ integration_config.go | 80 ++++++++++++++++++++++++++++++ integrations.go | 106 ---------------------------------------- 4 files changed, 176 insertions(+), 106 deletions(-) create mode 100644 buildingblock_runner.go create mode 100644 integration.go create mode 100644 integration_config.go delete mode 100644 integrations.go diff --git a/buildingblock_runner.go b/buildingblock_runner.go new file mode 100644 index 0000000..e73e102 --- /dev/null +++ b/buildingblock_runner.go @@ -0,0 +1,6 @@ +package client + +type BuildingBlockRunnerRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind" tfsdk:"kind"` +} diff --git a/integration.go b/integration.go new file mode 100644 index 0000000..8c6698a --- /dev/null +++ b/integration.go @@ -0,0 +1,90 @@ +package client + +import ( + "context" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types" +) + +type MeshIntegration struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata MeshIntegrationMetadata `json:"metadata"` + Spec MeshIntegrationSpec `json:"spec"` + Status *MeshIntegrationStatus `json:"status"` +} + +type MeshIntegrationMetadataAdapter[String any] struct { + Uuid String `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshIntegrationMetadata = MeshIntegrationMetadataAdapter[*types.String] + +type MeshIntegrationSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Config MeshIntegrationConfig `json:"config" tfsdk:"config"` +} + +type MeshIntegrationStatus struct { + IsBuiltIn bool `json:"isBuiltIn" tfsdk:"is_built_in"` + WorkloadIdentityFederation *MeshWorkloadIdentityFederation `json:"workloadIdentityFederation" tfsdk:"workload_identity_federation"` +} + +type MeshWorkloadIdentityFederation struct { + Issuer string `json:"issuer" tfsdk:"issuer"` + Subject string `json:"subject" tfsdk:"subject"` + Gcp *MeshWifProvider `json:"gcp" tfsdk:"gcp"` + Aws *MeshAwsWifProvider `json:"aws" tfsdk:"aws"` + Azure *MeshWifProvider `json:"azure" tfsdk:"azure"` +} + +type MeshWifProvider struct { + Audience string `json:"audience" tfsdk:"audience"` +} + +type MeshAwsWifProvider struct { + Audience string `json:"audience" tfsdk:"audience"` + Thumbprint string `json:"thumbprint" tfsdk:"thumbprint"` +} + +type MeshIntegrationClient interface { + Create(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) + Read(ctx context.Context, uuid string) (*MeshIntegration, error) + Update(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) + Delete(ctx context.Context, uuid string) error + List(ctx context.Context) ([]MeshIntegration, error) +} + +type meshIntegrationClientImpl struct { + meshObject internal.MeshObjectClient[MeshIntegration] +} + +func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) MeshIntegrationClient { + return &meshIntegrationClientImpl{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1-preview")} +} + +func (c meshIntegrationClientImpl) Create(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) { + integration.Kind = c.meshObject.Kind + integration.ApiVersion = c.meshObject.ApiVersion + return c.meshObject.Post(ctx, integration) +} + +func (c meshIntegrationClientImpl) Read(ctx context.Context, uuid string) (*MeshIntegration, error) { + return c.meshObject.Get(ctx, uuid) +} + +func (c meshIntegrationClientImpl) Update(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) { + integration.Kind = c.meshObject.Kind + integration.ApiVersion = c.meshObject.ApiVersion + return c.meshObject.Put(ctx, *integration.Metadata.Uuid, integration) +} + +func (c meshIntegrationClientImpl) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) +} + +func (c meshIntegrationClientImpl) List(ctx context.Context) ([]MeshIntegration, error) { + return c.meshObject.List(ctx) +} diff --git a/integration_config.go b/integration_config.go new file mode 100644 index 0000000..98f6f53 --- /dev/null +++ b/integration_config.go @@ -0,0 +1,80 @@ +package client + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" +) + +type MeshIntegrationConfigType string + +var ( + MeshIntegrationConfigTypes = enum.Enum[MeshIntegrationConfigType]{} + MeshIntegrationConfigTypeGithub = MeshIntegrationConfigTypes.Entry("github") + MeshIntegrationConfigTypeGitlab = MeshIntegrationConfigTypes.Entry("gitlab") + MeshIntegrationConfigTypeAzureDevops = MeshIntegrationConfigTypes.Entry("azuredevops") +) + +type MeshIntegrationGithubConfig struct { + Owner string `json:"owner" tfsdk:"owner"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + AppId string `json:"appId" tfsdk:"app_id"` + AppPrivateKey string `json:"appPrivateKey" tfsdk:"app_private_key"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +type MeshIntegrationGitlabConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +type MeshIntegrationAzureDevopsConfig struct { + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + Organization string `json:"organization" tfsdk:"organization"` + PersonalAccessToken string `json:"personalAccessToken" tfsdk:"personal_access_token"` + RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` +} + +type MeshIntegrationConfig struct { + Type enum.Entry[MeshIntegrationConfigType] `json:"type" tfsdk:"-"` + Github *MeshIntegrationGithubConfig `json:"github,omitempty" tfsdk:"github"` + Gitlab *MeshIntegrationGitlabConfig `json:"gitlab,omitempty" tfsdk:"gitlab"` + AzureDevops *MeshIntegrationAzureDevopsConfig `json:"azuredevops,omitempty" tfsdk:"azuredevops"` +} + +func (m MeshIntegrationConfig) InferTypeFromNonNilField() (result enum.Entry[MeshIntegrationConfigType]) { + setResultIfNotNil := func(implType enum.Entry[MeshIntegrationConfigType], v any) { + if !reflect.ValueOf(v).IsZero() { + if len(result) > 0 && result != implType { + panic(fmt.Errorf("inferred config type %s but already set to %s", implType, result)) + } + result = implType + } + } + setResultIfNotNil(MeshIntegrationConfigTypeGithub, m.Github) + setResultIfNotNil(MeshIntegrationConfigTypeGitlab, m.Gitlab) + setResultIfNotNil(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops) + if len(result) == 0 { + panic("cannot infer config type") + } + return +} + +func (m MeshIntegrationConfig) MarshalJSON() ([]byte, error) { + m.Type = m.InferTypeFromNonNilField() + // Using wrapped type avoids calling MarshalJSON recursively! + type wrapped MeshIntegrationConfig + return json.Marshal(wrapped(m)) +} + +func (m *MeshIntegrationConfig) UnmarshalJSON(bytes []byte) error { + type wrapped MeshIntegrationConfig + var target wrapped + if err := json.Unmarshal(bytes, &target); err != nil { + return err + } + *m = MeshIntegrationConfig(target) + return nil +} diff --git a/integrations.go b/integrations.go deleted file mode 100644 index df2643e..0000000 --- a/integrations.go +++ /dev/null @@ -1,106 +0,0 @@ -package client - -import ( - "context" - - "github.com/meshcloud/terraform-provider-meshstack/client/internal" -) - -type MeshIntegration struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshIntegrationMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshIntegrationSpec `json:"spec" tfsdk:"spec"` - Status *MeshIntegrationStatus `json:"status,omitempty" tfsdk:"status"` -} - -type MeshIntegrationMetadata struct { - Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - CreatedOn *string `json:"createdOn,omitempty" tfsdk:"created_on"` -} - -type MeshIntegrationSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Config MeshIntegrationConfig `json:"config" tfsdk:"config"` -} - -type MeshIntegrationStatus struct { - IsBuiltIn bool `json:"isBuiltIn" tfsdk:"is_built_in"` - WorkloadIdentityFederation *MeshWorkloadIdentityFederation `json:"workloadIdentityFederation,omitempty" tfsdk:"workload_identity_federation"` -} - -// Integration Config wrapper with type discrimination. -type MeshIntegrationConfig struct { - Type string `json:"type" tfsdk:"type"` - Github *MeshIntegrationGithubConfig `json:"github,omitempty" tfsdk:"github"` - Gitlab *MeshIntegrationGitlabConfig `json:"gitlab,omitempty" tfsdk:"gitlab"` - AzureDevops *MeshIntegrationAzureDevopsConfig `json:"azuredevops,omitempty" tfsdk:"azuredevops"` -} - -// GitHub Integration. -type MeshIntegrationGithubConfig struct { - Owner string `json:"owner" tfsdk:"owner"` - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - AppId string `json:"appId" tfsdk:"app_id"` - AppPrivateKey string `json:"appPrivateKey" tfsdk:"app_private_key"` - RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` -} - -// GitLab Integration. -type MeshIntegrationGitlabConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` -} - -// Azure DevOps Integration. -type MeshIntegrationAzureDevopsConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - Organization string `json:"organization" tfsdk:"organization"` - PersonalAccessToken string `json:"personalAccessToken" tfsdk:"personal_access_token"` - RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` -} - -// Building Block Runner Reference. -type BuildingBlockRunnerRef struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - Kind string `json:"kind" tfsdk:"kind"` -} - -// Workload Identity Federation. -type MeshWorkloadIdentityFederation struct { - Issuer string `json:"issuer" tfsdk:"issuer"` - Subject string `json:"subject" tfsdk:"subject"` - Gcp *MeshWifProvider `json:"gcp,omitempty" tfsdk:"gcp"` - Aws *MeshAwsWifProvider `json:"aws,omitempty" tfsdk:"aws"` - Azure *MeshWifProvider `json:"azure,omitempty" tfsdk:"azure"` -} - -type MeshWifProvider struct { - Audience string `json:"audience" tfsdk:"audience"` -} - -type MeshAwsWifProvider struct { - Audience string `json:"audience" tfsdk:"audience"` - Thumbprint string `json:"thumbprint" tfsdk:"thumbprint"` -} - -type MeshIntegrationClient struct { - meshObject internal.MeshObjectClient[MeshIntegration] -} - -func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) MeshIntegrationClient { - return MeshIntegrationClient{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1-preview")} -} - -func (c MeshIntegrationClient) integrationId(workspace string, uuid string) string { - return workspace + "/" + uuid -} - -func (c MeshIntegrationClient) Read(ctx context.Context, workspace string, uuid string) (*MeshIntegration, error) { - return c.meshObject.Get(ctx, c.integrationId(workspace, uuid)) -} - -func (c MeshIntegrationClient) List(ctx context.Context) ([]MeshIntegration, error) { - return c.meshObject.List(ctx) -} From 5ed8ad78516bcdbfd72402f6bbcd1cc274f9acfc Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Sun, 8 Feb 2026 19:37:07 +0100 Subject: [PATCH 104/200] feat: use default runner in meshstack_integration if omitted, add ref output, Secret support, better tests --- integration.go | 19 ++++++++----------- integration_config.go | 23 ++++++++++++----------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/integration.go b/integration.go index 8c6698a..80e583a 100644 --- a/integration.go +++ b/integration.go @@ -4,24 +4,21 @@ import ( "context" "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" ) type MeshIntegration struct { - ApiVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Metadata MeshIntegrationMetadata `json:"metadata"` - Spec MeshIntegrationSpec `json:"spec"` - Status *MeshIntegrationStatus `json:"status"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` + Kind string `json:"kind" tfsdk:"-"` + Metadata MeshIntegrationMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshIntegrationSpec `json:"spec" tfsdk:"spec"` + Status *MeshIntegrationStatus `json:"status" tfsdk:"status"` } -type MeshIntegrationMetadataAdapter[String any] struct { - Uuid String `json:"uuid,omitempty" tfsdk:"uuid"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +type MeshIntegrationMetadata struct { + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -type MeshIntegrationMetadata = MeshIntegrationMetadataAdapter[*types.String] - type MeshIntegrationSpec struct { DisplayName string `json:"displayName" tfsdk:"display_name"` Config MeshIntegrationConfig `json:"config" tfsdk:"config"` diff --git a/integration_config.go b/integration_config.go index 98f6f53..5b5b680 100644 --- a/integration_config.go +++ b/integration_config.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/meshcloud/terraform-provider-meshstack/client/types" "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) @@ -18,23 +19,23 @@ var ( ) type MeshIntegrationGithubConfig struct { - Owner string `json:"owner" tfsdk:"owner"` - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - AppId string `json:"appId" tfsdk:"app_id"` - AppPrivateKey string `json:"appPrivateKey" tfsdk:"app_private_key"` - RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + Owner string `json:"owner" tfsdk:"owner"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + AppId string `json:"appId" tfsdk:"app_id"` + AppPrivateKey types.Secret `json:"appPrivateKey" tfsdk:"app_private_key"` + RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } type MeshIntegrationGitlabConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } type MeshIntegrationAzureDevopsConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - Organization string `json:"organization" tfsdk:"organization"` - PersonalAccessToken string `json:"personalAccessToken" tfsdk:"personal_access_token"` - RunnerRef BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + Organization string `json:"organization" tfsdk:"organization"` + PersonalAccessToken types.Secret `json:"personalAccessToken" tfsdk:"personal_access_token"` + RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } type MeshIntegrationConfig struct { From 8c376281b13103c2b4c90a7a15a494af174937ba Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Sat, 10 Jan 2026 20:25:24 +0100 Subject: [PATCH 105/200] feat: add client.BuildingBlockDefinition(version) with model --- buildingblock_definition.go | 110 +++++++++ buildingblock_definition_version.go | 225 ++++++++++++++++++ ...block_definition_version_implementation.go | 116 +++++++++ buildingblock_definition_version_test.go | 52 ++++ client.go | 40 ++-- testdata/bbd_input/empty.json | 1 + testdata/bbd_input/not_sensitive.json | 5 + .../bbd_input/not_sensitive_but_hash.json | 6 + testdata/bbd_input/sensitive.json | 6 + testdata/bbd_input/sensitive_but_no_hash.json | 4 + types/clienttypes.go | 10 +- types/clienttypes_test.go | 46 ++++ 12 files changed, 600 insertions(+), 21 deletions(-) create mode 100644 buildingblock_definition.go create mode 100644 buildingblock_definition_version.go create mode 100644 buildingblock_definition_version_implementation.go create mode 100644 buildingblock_definition_version_test.go create mode 100644 testdata/bbd_input/empty.json create mode 100644 testdata/bbd_input/not_sensitive.json create mode 100644 testdata/bbd_input/not_sensitive_but_hash.json create mode 100644 testdata/bbd_input/sensitive.json create mode 100644 testdata/bbd_input/sensitive_but_no_hash.json create mode 100644 types/clienttypes_test.go diff --git a/buildingblock_definition.go b/buildingblock_definition.go new file mode 100644 index 0000000..4323804 --- /dev/null +++ b/buildingblock_definition.go @@ -0,0 +1,110 @@ +package client + +import ( + "context" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" +) + +type MeshBuildingBlockType string + +var ( + MeshBuildingBlockTypes = enum.Enum[MeshBuildingBlockType]{} + MeshBuildingBlockTypeTenantLevel = MeshBuildingBlockTypes.Entry("TENANT_LEVEL") + MeshBuildingBlockTypeWorkspaceLevel = MeshBuildingBlockTypes.Entry("WORKSPACE_LEVEL") +) + +type MeshBuildingBlockDefinitionMetadata struct { + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +type BuildingBlockDefinitionSupportedPlatform string + +type MeshBuildingBlockDefinitionSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + TargetType MeshBuildingBlockType `json:"targetType" tfsdk:"target_type"` + Description string `json:"description" tfsdk:"description"` + Readme *string `json:"readme,omitempty" tfsdk:"readme"` + RunTransparency bool `json:"runTransparency" tfsdk:"run_transparency"` + UseInLandingZonesOnly bool `json:"useInLandingZonesOnly" tfsdk:"use_in_landing_zones_only"` + SupportURL *string `json:"supportUrl,omitempty" tfsdk:"support_url"` + DocumentationURL *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + // NotificationSubscribers can also specify emails with prefix 'email:', so it's not only usernames (as the JSON field name suggests)! + NotificationSubscribers []string `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` + Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` + // SupportedPlatforms are currently platform types only. Specifying single platforms is currently unsupported. + // Have this list of string with a dedicated type, to convert it to/from Platform Type refs. + SupportedPlatforms []BuildingBlockDefinitionSupportedPlatform `json:"supportedPlatforms" tfsdk:"supported_platforms"` +} + +type MeshBuildingBlockDefinitionStatusVersion struct { + VersionUuid string `json:"versionUuid"` + VersionNumber int64 `json:"versionNumber"` + State MeshBuildingBlockDefinitionVersionState `json:"state"` +} + +type MeshBuildingBlockDefinitionStatus struct { + UsageCount *int64 `json:"usageCount"` + Versions []MeshBuildingBlockDefinitionStatusVersion `json:"versions"` + LatestVersion int64 `json:"latestVersion"` + LatestVersionUuid string `json:"latestVersionUuid"` + LatestReleasedVersion *int64 `json:"latestReleasedVersion"` + LatestReleasedVersionUuid *string `json:"latestReleasedVersionUuid"` +} + +type MeshBuildingBlockDefinition struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata MeshBuildingBlockDefinitionMetadata `json:"metadata"` + Spec MeshBuildingBlockDefinitionSpec `json:"spec"` + Status *MeshBuildingBlockDefinitionStatus `json:"status,omitempty"` +} + +type MeshBuildingBlockDefinitionClient interface { + List(ctx context.Context, workspaceIdentifier *string) ([]MeshBuildingBlockDefinition, error) + Read(ctx context.Context, uuid string) (*MeshBuildingBlockDefinition, error) + Create(ctx context.Context, definition MeshBuildingBlockDefinition) (*MeshBuildingBlockDefinition, error) + Update(ctx context.Context, uuid string, definition MeshBuildingBlockDefinition) (*MeshBuildingBlockDefinition, error) + Delete(ctx context.Context, uuid string) error +} + +type meshBuildingBlockDefinitionClient struct { + meshObject internal.MeshObjectClient[MeshBuildingBlockDefinition] +} + +func newBuildingBlockDefinitionClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockDefinitionClient { + return meshBuildingBlockDefinitionClient{ + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinition](ctx, httpClient, "v1-preview"), + } +} + +func (c meshBuildingBlockDefinitionClient) List(ctx context.Context, workspaceIdentifier *string) ([]MeshBuildingBlockDefinition, error) { + var options []internal.RequestOption + if workspaceIdentifier != nil { + options = append(options, internal.WithUrlQuery("workspaceIdentifier", *workspaceIdentifier)) + } + return c.meshObject.List(ctx, options...) +} + +func (c meshBuildingBlockDefinitionClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlockDefinition, error) { + return c.meshObject.Get(ctx, uuid) +} + +func (c meshBuildingBlockDefinitionClient) Create(ctx context.Context, definition MeshBuildingBlockDefinition) (*MeshBuildingBlockDefinition, error) { + definition.Kind = c.meshObject.Kind + definition.ApiVersion = c.meshObject.ApiVersion + return c.meshObject.Post(ctx, definition) +} + +func (c meshBuildingBlockDefinitionClient) Update(ctx context.Context, uuid string, definition MeshBuildingBlockDefinition) (*MeshBuildingBlockDefinition, error) { + definition.Kind = c.meshObject.Kind + definition.ApiVersion = c.meshObject.ApiVersion + return c.meshObject.Put(ctx, uuid, definition) +} + +func (c meshBuildingBlockDefinitionClient) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) +} diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go new file mode 100644 index 0000000..4ba8623 --- /dev/null +++ b/buildingblock_definition_version.go @@ -0,0 +1,225 @@ +package client + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" +) + +// Enums + +type MeshBuildingBlockDefinitionVersionState string + +var ( + MeshBuildingBlockDefinitionVersionStates = enum.Enum[MeshBuildingBlockDefinitionVersionState]{} + MeshBuildingBlockDefinitionVersionStateDraft = MeshBuildingBlockDefinitionVersionStates.Entry("DRAFT") + MeshBuildingBlockDefinitionVersionStateReleased = MeshBuildingBlockDefinitionVersionStates.Entry("RELEASED") +) + +type BuildingBlockDeletionMode string + +var ( + BuildingBlockDeletionModes = enum.Enum[BuildingBlockDeletionMode]{} + BuildingBlockDeletionModeDelete = BuildingBlockDeletionModes.Entry("DELETE") + BuildingBlockDeletionModePurge = BuildingBlockDeletionModes.Entry("PURGE") +) + +type MeshBuildingBlockIOType string + +var ( + MeshBuildingBlockIOTypes = enum.Enum[MeshBuildingBlockIOType]{} + MeshBuildingBlockIOTypeString = MeshBuildingBlockIOTypes.Entry("STRING") + MeshBuildingBlockIOTypeCode = MeshBuildingBlockIOTypes.Entry("CODE") + MeshBuildingBlockIOTypeInteger = MeshBuildingBlockIOTypes.Entry("INTEGER") + MeshBuildingBlockIOTypeBoolean = MeshBuildingBlockIOTypes.Entry("BOOLEAN") + MeshBuildingBlockIOTypeFile = MeshBuildingBlockIOTypes.Entry("FILE") + MeshBuildingBlockIOTypeList = MeshBuildingBlockIOTypes.Entry("LIST") + MeshBuildingBlockIOTypeSingleSelect = MeshBuildingBlockIOTypes.Entry("SINGLE_SELECT") + MeshBuildingBlockIOTypeMultiSelect = MeshBuildingBlockIOTypes.Entry("MULTI_SELECT") +) + +type MeshBuildingBlockInputAssignmentType string + +var ( + MeshBuildingBlockInputAssignmentTypes = enum.Enum[MeshBuildingBlockInputAssignmentType]{} + MeshBuildingBlockInputAssignmentTypeAuthor = MeshBuildingBlockInputAssignmentTypes.Entry("AUTHOR") + MeshBuildingBlockInputAssignmentTypeUserInput = MeshBuildingBlockInputAssignmentTypes.Entry("USER_INPUT") + MeshBuildingBlockInputAssignmentTypePlatformOperatorManualInput = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_OPERATOR_MANUAL_INPUT") + MeshBuildingBlockInputAssignmentTypeBuildingBlockOutput = MeshBuildingBlockInputAssignmentTypes.Entry("BUILDING_BLOCK_OUTPUT") + MeshBuildingBlockInputAssignmentTypePlatformTenantID = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_TENANT_ID") + MeshBuildingBlockInputAssignmentTypeWorkspaceIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("WORKSPACE_IDENTIFIER") + MeshBuildingBlockInputAssignmentTypeProjectIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("PROJECT_IDENTIFIER") + MeshBuildingBlockInputAssignmentTypeFullPlatformIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("FULL_PLATFORM_IDENTIFIER") + MeshBuildingBlockInputAssignmentTypeTenantBuildingBlockUuid = MeshBuildingBlockInputAssignmentTypes.Entry("TENANT_BUILDING_BLOCK_UUID") + MeshBuildingBlockInputAssignmentTypeStatic = MeshBuildingBlockInputAssignmentTypes.Entry("STATIC") + MeshBuildingBlockInputAssignmentTypeUserPermissions = MeshBuildingBlockInputAssignmentTypes.Entry("USER_PERMISSIONS") +) + +type MeshBuildingBlockDefinitionOutputAssignmentType string + +var ( + MeshBuildingBlockDefinitionOutputAssignmentTypes = enum.Enum[MeshBuildingBlockDefinitionOutputAssignmentType]{} + MeshBuildingBlockDefinitionOutputAssignmentTypeNone = MeshBuildingBlockDefinitionOutputAssignmentTypes.Entry("NONE") + MeshBuildingBlockDefinitionOutputAssignmentTypePlatformTenantID = MeshBuildingBlockDefinitionOutputAssignmentTypes.Entry("PLATFORM_TENANT_ID") + MeshBuildingBlockDefinitionOutputAssignmentTypeSignInURL = MeshBuildingBlockDefinitionOutputAssignmentTypes.Entry("SIGN_IN_URL") + MeshBuildingBlockDefinitionOutputAssignmentTypeResourceURL = MeshBuildingBlockDefinitionOutputAssignmentTypes.Entry("RESOURCE_URL") + MeshBuildingBlockDefinitionOutputAssignmentTypeSummary = MeshBuildingBlockDefinitionOutputAssignmentTypes.Entry("SUMMARY") +) + +// Ref types + +type BuildingBlockDefinitionRef struct { + Uuid string `json:"uuid"` + Kind string `json:"kind"` +} + +type MeshIntegrationRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind" tfsdk:"kind"` +} + +// Input and Output types + +type MeshBuildingBlockDefinitionInput struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Type MeshBuildingBlockIOType `json:"type" tfsdk:"type"` + AssignmentType MeshBuildingBlockInputAssignmentType `json:"assignmentType" tfsdk:"assignment_type"` + IsEnvironment bool `json:"isEnvironment" tfsdk:"is_environment"` + IsSensitive bool `json:"isSensitive" tfsdk:"-"` + // If IsSensitive is true, the [types.Variant] (typedef [types.SecretOrAny]) for fields + // MeshBuildingBlockDefinitionInputAdapter.Argument and + // MeshBuildingBlockDefinitionInputAdapter.DefaultValue + // is of [types.Secret] (case [types.Variant.X]). + // Otherwise, the [types.Variant] is of [types.Any] (case [types.Variant.Y]). + // As this is a fallback detection when JSON (un)marshaling, + // types.Any must go second as [types.Variant] intentionally prefers X over Y. + Argument types.SecretOrAny `json:"argument,omitempty" tfsdk:"argument"` + DefaultValue types.SecretOrAny `json:"defaultValue,omitempty" tfsdk:"default_value"` + UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` + SelectableValues []types.SetElem `json:"selectableValues,omitempty" tfsdk:"selectable_values"` + Description *string `json:"description,omitempty" tfsdk:"description"` + ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` + ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` +} + +func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSON(bytes []byte) error { + type wrapped MeshBuildingBlockDefinitionInput + var target wrapped + if err := json.Unmarshal(bytes, &target); err != nil { + return err + } + *m = MeshBuildingBlockDefinitionInput(target) + switch { + case !m.IsSensitive: + // ensure "any" struct fields never end up in X accidentally, + // as X is only set when IsSensitive is true! + var errs []error + moveXtoYIfPresent := func(v *types.SecretOrAny) { + if v.HasX() { + xJson, err := json.Marshal(v.X) + errs = append(errs, err) + v.X = types.Secret{} + errs = append(errs, json.Unmarshal(xJson, &v.Y)) + } + } + moveXtoYIfPresent(&m.Argument) + moveXtoYIfPresent(&m.DefaultValue) + return errors.Join(errs...) + case m.Argument.HasY(), m.DefaultValue.HasY(): + return fmt.Errorf("got sensitive argument or default_value but variant Y is set instead") + default: + return nil + } +} + +type MeshBuildingBlockDefinitionOutput struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Type MeshBuildingBlockIOType `json:"type" tfsdk:"type"` + AssignmentType MeshBuildingBlockDefinitionOutputAssignmentType `json:"assignmentType" tfsdk:"assignment_type"` +} + +// Main version types + +type MeshBuildingBlockDefinitionVersionMetadata struct { + Uuid string `json:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace"` + CreatedOn string `json:"createdOn"` +} + +type BuildingBlockDependencyRef string +type MeshBuildingBlockDefinitionVersionSpec struct { + BuildingBlockDefinitionRef *BuildingBlockDefinitionRef `json:"buildingBlockDefinitionRef" tfsdk:"-"` + OnlyApplyOncePerTenant bool `json:"onlyApplyOncePerTenant" tfsdk:"only_apply_once_per_tenant"` + DeletionMode BuildingBlockDeletionMode `json:"deletionMode" tfsdk:"deletion_mode"` + Outputs map[string]MeshBuildingBlockDefinitionOutput `json:"outputs" tfsdk:"outputs"` + VersionNumber *int64 `json:"versionNumber,omitempty" tfsdk:"version_number"` + State *MeshBuildingBlockDefinitionVersionState `json:"state,omitempty" tfsdk:"state"` + RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + DependencyDefinitionUUIDs []BuildingBlockDependencyRef `json:"dependencyDefinitionUuids,omitempty" tfsdk:"dependency_refs"` + Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` + Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` +} + +type MeshBuildingBlockDefinitionVersionStatus struct { + State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` + UsageCount int64 `json:"usageCount" tfsdk:"usage_count"` +} + +type MeshBuildingBlockDefinitionVersion struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshBuildingBlockDefinitionVersionMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockDefinitionVersionSpec `json:"spec" tfsdk:"spec"` + Status *MeshBuildingBlockDefinitionVersionStatus `json:"status,omitempty" tfsdk:"status"` +} + +// MeshBuildingBlockDefinitionVersionClient manages a version of a building block definition. +// As such a version is tightly coupled to the definition, there's no single Get or Delete implemented. +// A Get is not required as we always expose all versions of a definition anyway, and a Delete happens together when the definition is deleted. +type MeshBuildingBlockDefinitionVersionClient interface { + List(ctx context.Context, buildingBlockDefinitionUuid string) ([]MeshBuildingBlockDefinitionVersion, error) + Create(ctx context.Context, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) + Update(ctx context.Context, uuid, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) +} + +type meshBuildingBlockDefinitionVersionClient struct { + meshObject internal.MeshObjectClient[MeshBuildingBlockDefinitionVersion] +} + +func newBuildingBlockDefinitionVersionClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockDefinitionVersionClient { + return meshBuildingBlockDefinitionVersionClient{ + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinitionVersion](ctx, httpClient, "v1-preview"), + } +} + +func (c meshBuildingBlockDefinitionVersionClient) List(ctx context.Context, buildingBlockDefinitionUuid string) ([]MeshBuildingBlockDefinitionVersion, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery("buildingBlockDefinitionUuid", buildingBlockDefinitionUuid)) +} + +func (c meshBuildingBlockDefinitionVersionClient) Create(ctx context.Context, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) { + return c.meshObject.Post(ctx, MeshBuildingBlockDefinitionVersion{ + ApiVersion: c.meshObject.ApiVersion, + Kind: c.meshObject.Kind, + Metadata: MeshBuildingBlockDefinitionVersionMetadata{ + OwnedByWorkspace: ownedByWorkspace, + }, + Spec: versionSpec, + }) +} + +func (c meshBuildingBlockDefinitionVersionClient) Update(ctx context.Context, uuid, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) { + return c.meshObject.Put(ctx, uuid, MeshBuildingBlockDefinitionVersion{ + ApiVersion: c.meshObject.ApiVersion, + Kind: c.meshObject.Kind, + Metadata: MeshBuildingBlockDefinitionVersionMetadata{ + Uuid: uuid, + OwnedByWorkspace: ownedByWorkspace, + }, + Spec: versionSpec, + }) +} diff --git a/buildingblock_definition_version_implementation.go b/buildingblock_definition_version_implementation.go new file mode 100644 index 0000000..f8d169d --- /dev/null +++ b/buildingblock_definition_version_implementation.go @@ -0,0 +1,116 @@ +package client + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" +) + +type MeshBuildingBlockImplementationType string + +var ( + MeshBuildingBlockImplementationTypes = enum.Enum[MeshBuildingBlockImplementationType]{} + MeshBuildingBlockImplementationTypeManual = MeshBuildingBlockImplementationTypes.Entry("manual") + MeshBuildingBlockImplementationTypeTerraform = MeshBuildingBlockImplementationTypes.Entry("terraform") + MeshBuildingBlockImplementationTypeGithubWorkflows = MeshBuildingBlockImplementationTypes.Entry("githubWorkflows") + MeshBuildingBlockImplementationTypeGitlabPipeline = MeshBuildingBlockImplementationTypes.Entry("gitlabPipeline") + MeshBuildingBlockImplementationTypeAzureDevOpsPipeline = MeshBuildingBlockImplementationTypes.Entry("azureDevOpsPipeline") +) + +type MeshBuildingBlockDefinitionSshKnownHost struct { + Host string `json:"host" tfsdk:"host"` + KeyType string `json:"keyType" tfsdk:"key_type"` + KeyValue string `json:"keyValue" tfsdk:"key_value"` +} + +type MeshBuildingBlockDefinitionTerraformImplementation struct { + TerraformVersion string `json:"terraformVersion" tfsdk:"terraform_version"` + RepositoryURL string `json:"repositoryUrl" tfsdk:"repository_url"` + Async bool `json:"async" tfsdk:"async"` + RepositoryPath *string `json:"repositoryPath,omitempty" tfsdk:"repository_path"` + RefName *string `json:"refName,omitempty" tfsdk:"ref_name"` + SSHKnownHost *MeshBuildingBlockDefinitionSshKnownHost `json:"sshKnownHost,omitempty" tfsdk:"ssh_known_host"` + UseMeshHTTPBackendFallback bool `json:"useMeshHttpBackendFallback" tfsdk:"use_mesh_http_backend_fallback"` + SSHPrivateKey *types.Secret `json:"sshPrivateKey,omitempty" tfsdk:"ssh_private_key"` +} + +type MeshBuildingBlockDefinitionGitHubWorkflowsImplementation struct { + Repository string `json:"repository" tfsdk:"repository"` + Branch string `json:"branch" tfsdk:"branch"` + ApplyWorkflow string `json:"applyWorkflow" tfsdk:"apply_workflow"` + DestroyWorkflow *string `json:"destroyWorkflow" tfsdk:"destroy_workflow"` + Async bool `json:"async" tfsdk:"async"` + OmitRunObjectInput bool `json:"omitRunObjectInput" tfsdk:"omit_run_object_input"` + IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` +} + +type MeshBuildingBlockDefinitionManualImplementation struct { +} + +type MeshBuildingBlockDefinitionGitLabPipelineImplementation struct { + ProjectID string `json:"projectId" tfsdk:"project_id"` + RefName string `json:"refName" tfsdk:"ref_name"` + IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` + PipelineTriggerToken types.Secret `json:"pipelineTriggerToken" tfsdk:"pipeline_trigger_token"` +} + +type MeshBuildingBlockDefinitionAzureDevOpsPipelineImplementation struct { + Project string `json:"project" tfsdk:"project"` + PipelineID string `json:"pipelineId" tfsdk:"pipeline_id"` + Async bool `json:"async" tfsdk:"async"` + IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` +} + +type MeshBuildingBlockDefinitionImplementation struct { + Type enum.Entry[MeshBuildingBlockImplementationType] `json:"type" tfsdk:"-"` + Manual *MeshBuildingBlockDefinitionManualImplementation `json:"manual,omitempty" tfsdk:"manual"` + GithubWorkflows *MeshBuildingBlockDefinitionGitHubWorkflowsImplementation `json:"githubWorkflows,omitempty" tfsdk:"github_workflows"` + AzureDevOpsPipeline *MeshBuildingBlockDefinitionAzureDevOpsPipelineImplementation `json:"azureDevOpsPipeline,omitempty" tfsdk:"azure_devops_pipeline"` + GitlabPipeline *MeshBuildingBlockDefinitionGitLabPipelineImplementation `json:"gitlabPipeline,omitempty" tfsdk:"gitlab_pipeline"` + Terraform *MeshBuildingBlockDefinitionTerraformImplementation `json:"terraform,omitempty" tfsdk:"terraform"` +} + +func (m MeshBuildingBlockDefinitionImplementation) InferTypeFromNonNilField() (result enum.Entry[MeshBuildingBlockImplementationType]) { + setResultIfNotNil := func(implType enum.Entry[MeshBuildingBlockImplementationType], v any) { + // Manual implementation is an empty struct, so carefully check v for nilness using reflection! + if !reflect.ValueOf(v).IsZero() { + if len(result) > 0 && result != implType { + panic(fmt.Errorf("inferred implementation type %s but already set to %s", implType, result)) + } + result = implType + } + } + setResultIfNotNil(MeshBuildingBlockImplementationTypeManual, m.Manual) + setResultIfNotNil(MeshBuildingBlockImplementationTypeTerraform, m.Terraform) + setResultIfNotNil(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows) + setResultIfNotNil(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline) + setResultIfNotNil(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline) + if len(result) == 0 { + panic("cannot infer implementation type") + } + return +} + +func (m MeshBuildingBlockDefinitionImplementation) MarshalJSON() ([]byte, error) { + if len(m.Type) == 0 { + m.Type = m.InferTypeFromNonNilField() + } + type wrapped MeshBuildingBlockDefinitionImplementation + return json.Marshal(wrapped(m)) +} + +func (m *MeshBuildingBlockDefinitionImplementation) UnmarshalJSON(bytes []byte) error { + type wrapped MeshBuildingBlockDefinitionImplementation + var target wrapped + if err := json.Unmarshal(bytes, &target); err != nil { + return err + } + *m = MeshBuildingBlockDefinitionImplementation(target) + if m.Type == MeshBuildingBlockImplementationTypeManual { + m.Manual = &MeshBuildingBlockDefinitionManualImplementation{} + } + return nil +} diff --git a/buildingblock_definition_version_test.go b/buildingblock_definition_version_test.go new file mode 100644 index 0000000..9316d46 --- /dev/null +++ b/buildingblock_definition_version_test.go @@ -0,0 +1,52 @@ +package client + +import ( + "embed" + "encoding/json" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types/ptr" +) + +var ( + //go:embed testdata/bbd_input + bbdInputTestdata embed.FS +) + +func TestMeshBuildingBlockDefinitionInput_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + wantSensitive bool + wantArgument types.SecretOrAny + wantDefaultValue types.SecretOrAny + wantErr assert.ErrorAssertionFunc + }{ + {"empty", false, types.SecretOrAny{}, types.SecretOrAny{}, assert.NoError}, + {"not_sensitive", false, types.SecretOrAny{Y: true}, types.SecretOrAny{Y: "some-string"}, assert.NoError}, + {"not_sensitive_but_hash", false, types.SecretOrAny{Y: map[string]any{"hash": "some-hash-looks-like-secret"}}, types.SecretOrAny{}, assert.NoError}, + {"sensitive", true, types.SecretOrAny{}, types.SecretOrAny{X: types.Secret{Hash: ptr.To("some-hash")}}, assert.NoError}, + {"sensitive_but_no_hash", true, types.SecretOrAny{Y: map[string]any{}}, types.SecretOrAny{}, func(t assert.TestingT, err error, msgAndArgs ...any) bool { + return assert.ErrorContains(t, err, "got sensitive argument or default_value but variant Y is set instead") + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonFile, err := bbdInputTestdata.ReadFile(path.Join("testdata/bbd_input", path.Base(tt.name)+".json")) + require.NoError(t, err) + var target MeshBuildingBlockDefinitionInput + if tt.wantErr(t, json.Unmarshal(jsonFile, &target)) { + expected := MeshBuildingBlockDefinitionInput{ + IsSensitive: tt.wantSensitive, + Argument: tt.wantArgument, + DefaultValue: tt.wantDefaultValue, + } + assert.Equal(t, expected, target) + } + }) + } +} diff --git a/client.go b/client.go index f55c598..e47a1f4 100644 --- a/client.go +++ b/client.go @@ -17,24 +17,26 @@ import ( var MinMeshStackVersion = version.MustParse("2026.2.0") type Client struct { - BuildingBlock MeshBuildingBlockClient - BuildingBlockV2 MeshBuildingBlockV2Client - Integration MeshIntegrationClient - LandingZone MeshLandingZoneClient - Location MeshLocationClient - PaymentMethod MeshPaymentMethodClient - Platform MeshPlatformClient - Project MeshProjectClient - ProjectGroupBinding MeshProjectGroupBindingClient - ProjectUserBinding MeshProjectUserBindingClient - ServiceInstance MeshServiceInstanceClient - TagDefinition MeshTagDefinitionClient - Tenant MeshTenantClient - TenantV4 MeshTenantV4Client - Workspace MeshWorkspaceClient - WorkspaceGroupBinding MeshWorkspaceGroupBindingClient - WorkspaceUserBinding MeshWorkspaceUserBindingClient - PlatformType MeshPlatformTypeClient + BuildingBlock MeshBuildingBlockClient + BuildingBlockV2 MeshBuildingBlockV2Client + BuildingBlockDefinition MeshBuildingBlockDefinitionClient + BuildingBlockDefinitionVersion MeshBuildingBlockDefinitionVersionClient + Integration MeshIntegrationClient + LandingZone MeshLandingZoneClient + Location MeshLocationClient + PaymentMethod MeshPaymentMethodClient + Platform MeshPlatformClient + Project MeshProjectClient + ProjectGroupBinding MeshProjectGroupBindingClient + ProjectUserBinding MeshProjectUserBindingClient + ServiceInstance MeshServiceInstanceClient + TagDefinition MeshTagDefinitionClient + Tenant MeshTenantClient + TenantV4 MeshTenantV4Client + Workspace MeshWorkspaceClient + WorkspaceGroupBinding MeshWorkspaceGroupBindingClient + WorkspaceUserBinding MeshWorkspaceUserBindingClient + PlatformType MeshPlatformTypeClient } func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string, apiToken string) (Client, error) { @@ -70,6 +72,8 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str return Client{ newBuildingBlockClient(ctx, httpClient), newBuildingBlockV2Client(ctx, httpClient), + newBuildingBlockDefinitionClient(ctx, httpClient), + newBuildingBlockDefinitionVersionClient(ctx, httpClient), newIntegrationClient(ctx, httpClient), newLandingZoneClient(ctx, httpClient), newLocationClient(ctx, httpClient), diff --git a/testdata/bbd_input/empty.json b/testdata/bbd_input/empty.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/testdata/bbd_input/empty.json @@ -0,0 +1 @@ +{} diff --git a/testdata/bbd_input/not_sensitive.json b/testdata/bbd_input/not_sensitive.json new file mode 100644 index 0000000..4bf4ef4 --- /dev/null +++ b/testdata/bbd_input/not_sensitive.json @@ -0,0 +1,5 @@ +{ + "isSensitive": false, + "argument": true, + "defaultValue": "some-string" +} diff --git a/testdata/bbd_input/not_sensitive_but_hash.json b/testdata/bbd_input/not_sensitive_but_hash.json new file mode 100644 index 0000000..49d2b6d --- /dev/null +++ b/testdata/bbd_input/not_sensitive_but_hash.json @@ -0,0 +1,6 @@ +{ + "isSensitive": false, + "argument": { + "hash": "some-hash-looks-like-secret" + } +} diff --git a/testdata/bbd_input/sensitive.json b/testdata/bbd_input/sensitive.json new file mode 100644 index 0000000..861803d --- /dev/null +++ b/testdata/bbd_input/sensitive.json @@ -0,0 +1,6 @@ +{ + "isSensitive": true, + "defaultValue": { + "hash": "some-hash" + } +} diff --git a/testdata/bbd_input/sensitive_but_no_hash.json b/testdata/bbd_input/sensitive_but_no_hash.json new file mode 100644 index 0000000..c9fc4ed --- /dev/null +++ b/testdata/bbd_input/sensitive_but_no_hash.json @@ -0,0 +1,4 @@ +{ + "isSensitive": true, + "argument": {} +} diff --git a/types/clienttypes.go b/types/clienttypes.go index 08d597d..50c7a87 100644 --- a/types/clienttypes.go +++ b/types/clienttypes.go @@ -1,12 +1,16 @@ package types +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/types/variant" +) + type ( - String = string - Number = int64 - Any = any + SetElem string Secret struct { Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` Hash *string `json:"hash,omitempty" tfsdk:"-"` } + + SecretOrAny = variant.Variant[Secret, any] ) diff --git a/types/clienttypes_test.go b/types/clienttypes_test.go new file mode 100644 index 0000000..39a3f99 --- /dev/null +++ b/types/clienttypes_test.go @@ -0,0 +1,46 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/terraform-provider-meshstack/client/types/ptr" +) + +func TestSecretOrAny(t *testing.T) { + type testCase struct { + name string + json string + v SecretOrAny + + wantX, wantY bool + } + tests := []testCase{ + {"empty", `null`, SecretOrAny{}, false, false}, + {"X plaintext", `{"plaintext":"some-secret"}`, SecretOrAny{X: Secret{Plaintext: ptr.To("some-secret")}}, true, false}, + {"Y string", `"some-string"`, SecretOrAny{Y: "some-string"}, false, true}, + {"Y bool", `true`, SecretOrAny{Y: true}, false, true}, + {"Y number", `1.23123`, SecretOrAny{Y: 1.23123}, false, true}, + {"Y other struct", `{"A":"aa","B":"bb"}`, SecretOrAny{Y: map[string]any{"A": "aa", "B": "bb"}}, false, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run("unmarshal", func(t *testing.T) { + var unmarshalled SecretOrAny + require.NoError(t, json.Unmarshal([]byte(tt.json), &unmarshalled)) + assert.Equal(t, tt.v, unmarshalled) + assert.Equal(t, tt.wantX, unmarshalled.HasX()) + assert.Equal(t, tt.wantY, unmarshalled.HasY()) + }) + + t.Run("marshal", func(t *testing.T) { + marshalled, err := json.Marshal(tt.v) + require.NoError(t, err) + assert.Equal(t, tt.json, string(marshalled)) + }) + }) + } +} From f62ac8b35b5eaa6f3fd7e670cbbdf7ecc8939061 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 11 Feb 2026 10:41:17 +0100 Subject: [PATCH 106/200] feat: add meshstack_building_block_definition --- buildingblock_definition_version_test.go | 4 ++-- .../empty.json | 0 .../not_sensitive.json | 0 .../not_sensitive_but_hash.json | 0 .../sensitive.json | 0 .../sensitive_but_no_hash.json | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename testdata/{bbd_input => building_block_definition_version_input}/empty.json (100%) rename testdata/{bbd_input => building_block_definition_version_input}/not_sensitive.json (100%) rename testdata/{bbd_input => building_block_definition_version_input}/not_sensitive_but_hash.json (100%) rename testdata/{bbd_input => building_block_definition_version_input}/sensitive.json (100%) rename testdata/{bbd_input => building_block_definition_version_input}/sensitive_but_no_hash.json (100%) diff --git a/buildingblock_definition_version_test.go b/buildingblock_definition_version_test.go index 9316d46..884a3ce 100644 --- a/buildingblock_definition_version_test.go +++ b/buildingblock_definition_version_test.go @@ -14,7 +14,7 @@ import ( ) var ( - //go:embed testdata/bbd_input + //go:embed testdata/building_block_definition_version_input bbdInputTestdata embed.FS ) @@ -36,7 +36,7 @@ func TestMeshBuildingBlockDefinitionInput_UnmarshalJSON(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - jsonFile, err := bbdInputTestdata.ReadFile(path.Join("testdata/bbd_input", path.Base(tt.name)+".json")) + jsonFile, err := bbdInputTestdata.ReadFile(path.Join("testdata/building_block_definition_version_input", path.Base(tt.name)+".json")) require.NoError(t, err) var target MeshBuildingBlockDefinitionInput if tt.wantErr(t, json.Unmarshal(jsonFile, &target)) { diff --git a/testdata/bbd_input/empty.json b/testdata/building_block_definition_version_input/empty.json similarity index 100% rename from testdata/bbd_input/empty.json rename to testdata/building_block_definition_version_input/empty.json diff --git a/testdata/bbd_input/not_sensitive.json b/testdata/building_block_definition_version_input/not_sensitive.json similarity index 100% rename from testdata/bbd_input/not_sensitive.json rename to testdata/building_block_definition_version_input/not_sensitive.json diff --git a/testdata/bbd_input/not_sensitive_but_hash.json b/testdata/building_block_definition_version_input/not_sensitive_but_hash.json similarity index 100% rename from testdata/bbd_input/not_sensitive_but_hash.json rename to testdata/building_block_definition_version_input/not_sensitive_but_hash.json diff --git a/testdata/bbd_input/sensitive.json b/testdata/building_block_definition_version_input/sensitive.json similarity index 100% rename from testdata/bbd_input/sensitive.json rename to testdata/building_block_definition_version_input/sensitive.json diff --git a/testdata/bbd_input/sensitive_but_no_hash.json b/testdata/building_block_definition_version_input/sensitive_but_no_hash.json similarity index 100% rename from testdata/bbd_input/sensitive_but_no_hash.json rename to testdata/building_block_definition_version_input/sensitive_but_no_hash.json From 0dc1f1a484e3c7fbc911cc8378569c9fa2467bac Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 11 Feb 2026 14:14:47 +0100 Subject: [PATCH 107/200] fix: handle empty argument/defaultValue correctly in SecretOrAny --- types/clienttypes_test.go | 1 + types/variant/variant.go | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/types/clienttypes_test.go b/types/clienttypes_test.go index 39a3f99..cfaeb95 100644 --- a/types/clienttypes_test.go +++ b/types/clienttypes_test.go @@ -24,6 +24,7 @@ func TestSecretOrAny(t *testing.T) { {"Y string", `"some-string"`, SecretOrAny{Y: "some-string"}, false, true}, {"Y bool", `true`, SecretOrAny{Y: true}, false, true}, {"Y number", `1.23123`, SecretOrAny{Y: 1.23123}, false, true}, + {"Y empty string", `""`, SecretOrAny{Y: ""}, false, true}, {"Y other struct", `{"A":"aa","B":"bb"}`, SecretOrAny{Y: map[string]any{"A": "aa", "B": "bb"}}, false, true}, } for _, tt := range tests { diff --git a/types/variant/variant.go b/types/variant/variant.go index 6795181..a7f3f66 100644 --- a/types/variant/variant.go +++ b/types/variant/variant.go @@ -31,14 +31,24 @@ func (v Variant[X, Y]) MarshalJSON() ([]byte, error) { } } +func has[T any](xy any) bool { + v := reflect.ValueOf(xy) + kind := reflect.TypeFor[T]().Kind() + if kind != reflect.Interface { + // T is not any (aka as a valid 'zero' representation) + return !v.IsZero() + } else { + // T is any, so we only check for validness + return v.IsValid() + } +} + func (v Variant[X, Y]) HasX() bool { - x := reflect.ValueOf(v.X) - return x.IsValid() && !x.IsZero() + return has[X](v.X) } func (v Variant[X, Y]) HasY() bool { - y := reflect.ValueOf(v.Y) - return y.IsValid() && !y.IsZero() + return has[Y](v.Y) } func (v Variant[X, Y]) WithX(action func(x *X)) { From 91d1926b171b8d0d6b3e5f8096db6d7136463cc0 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 13 Feb 2026 10:35:31 +0100 Subject: [PATCH 108/200] feat: add permissions to meshstack_building_block_definition.version_spec --- api_permissions.go | 86 +++++++++++++++++++++++++++++ buildingblock_definition_version.go | 1 + 2 files changed, 87 insertions(+) create mode 100644 api_permissions.go diff --git a/api_permissions.go b/api_permissions.go new file mode 100644 index 0000000..e7e89ed --- /dev/null +++ b/api_permissions.go @@ -0,0 +1,86 @@ +package client + +import ( + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" +) + +// API Permissions as defined in https://docs.meshcloud.io/api/authentication/api-permissions/ + +type ApiPermission string + +// Workspace Permissions (non-admin). +var ( + WorkspacePermissions = enum.Enum[ApiPermission]{} + + PermissionBuildingBlockDefinitionDelete = WorkspacePermissions.Entry("BUILDINGBLOCKDEFINITION_DELETE") + PermissionBuildingBlockDefinitionList = WorkspacePermissions.Entry("BUILDINGBLOCKDEFINITION_LIST") + PermissionBuildingBlockDefinitionSave = WorkspacePermissions.Entry("BUILDINGBLOCKDEFINITION_SAVE") + + PermissionBuildingBlockRunnerDelete = WorkspacePermissions.Entry("BUILDINGBLOCKRUNNER_DELETE") + PermissionBuildingBlockRunnerList = WorkspacePermissions.Entry("BUILDINGBLOCKRUNNER_LIST") + PermissionBuildingBlockRunnerSave = WorkspacePermissions.Entry("BUILDINGBLOCKRUNNER_SAVE") + + PermissionBuildingBlockDelete = WorkspacePermissions.Entry("BUILDINGBLOCK_DELETE") + PermissionBuildingBlockList = WorkspacePermissions.Entry("BUILDINGBLOCK_LIST") + PermissionBuildingBlockSave = WorkspacePermissions.Entry("BUILDINGBLOCK_SAVE") + + PermissionCommunicationDefinitionDelete = WorkspacePermissions.Entry("COMMUNICATIONDEFINITION_DELETE") + PermissionCommunicationDefinitionList = WorkspacePermissions.Entry("COMMUNICATIONDEFINITION_LIST") + PermissionCommunicationDefinitionSave = WorkspacePermissions.Entry("COMMUNICATIONDEFINITION_SAVE") + + PermissionCommunicationDelete = WorkspacePermissions.Entry("COMMUNICATION_DELETE") + PermissionCommunicationList = WorkspacePermissions.Entry("COMMUNICATION_LIST") + PermissionCommunicationSave = WorkspacePermissions.Entry("COMMUNICATION_SAVE") + + PermissionEventLogList = WorkspacePermissions.Entry("EVENTLOG_LIST") + + PermissionIntegrationDelete = WorkspacePermissions.Entry("INTEGRATION_DELETE") + PermissionIntegrationList = WorkspacePermissions.Entry("INTEGRATION_LIST") + PermissionIntegrationSave = WorkspacePermissions.Entry("INTEGRATION_SAVE") + + PermissionLandingZoneDelete = WorkspacePermissions.Entry("LANDINGZONE_DELETE") + PermissionLandingZoneList = WorkspacePermissions.Entry("LANDINGZONE_LIST") + PermissionLandingZoneSave = WorkspacePermissions.Entry("LANDINGZONE_SAVE") + + PermissionManagedBuildingBlockRunSourceSave = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCKRUNSOURCE_SAVE") + PermissionManagedBuildingBlockRunList = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCKRUN_LIST") + PermissionManagedBuildingBlockRunSave = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCKRUN_SAVE") + PermissionManagedBuildingBlockList = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCK_LIST") + PermissionManagedTenantImport = WorkspacePermissions.Entry("MANAGED_TENANT_IMPORT") + + PermissionPaymentMethodList = WorkspacePermissions.Entry("PAYMENTMETHOD_LIST") + + PermissionPlatformInstanceDelete = WorkspacePermissions.Entry("PLATFORMINSTANCE_DELETE") + PermissionPlatformInstanceList = WorkspacePermissions.Entry("PLATFORMINSTANCE_LIST") + PermissionPlatformInstanceSave = WorkspacePermissions.Entry("PLATFORMINSTANCE_SAVE") + + PermissionProjectPrincipalRoleDelete = WorkspacePermissions.Entry("PROJECTPRINCIPALROLE_DELETE") + PermissionProjectPrincipalRoleList = WorkspacePermissions.Entry("PROJECTPRINCIPALROLE_LIST") + PermissionProjectPrincipalRoleSave = WorkspacePermissions.Entry("PROJECTPRINCIPALROLE_SAVE") + + PermissionProjectDelete = WorkspacePermissions.Entry("PROJECT_DELETE") + PermissionProjectList = WorkspacePermissions.Entry("PROJECT_LIST") + PermissionProjectSave = WorkspacePermissions.Entry("PROJECT_SAVE") + + PermissionServiceInstanceDelete = WorkspacePermissions.Entry("SERVICEINSTANCE_DELETE") + PermissionServiceInstanceList = WorkspacePermissions.Entry("SERVICEINSTANCE_LIST") + PermissionServiceInstanceSave = WorkspacePermissions.Entry("SERVICEINSTANCE_SAVE") + + PermissionTenantDelete = WorkspacePermissions.Entry("TENANT_DELETE") + PermissionTenantList = WorkspacePermissions.Entry("TENANT_LIST") + PermissionTenantSave = WorkspacePermissions.Entry("TENANT_SAVE") + + PermissionTfStateDelete = WorkspacePermissions.Entry("TFSTATE_DELETE") + PermissionTfStateList = WorkspacePermissions.Entry("TFSTATE_LIST") + PermissionTfStateSave = WorkspacePermissions.Entry("TFSTATE_SAVE") + + PermissionWorkspacePrincipalBindingDelete = WorkspacePermissions.Entry("WORKSPACEPRINCIPALBINDING_DELETE") + PermissionWorkspacePrincipalBindingList = WorkspacePermissions.Entry("WORKSPACEPRINCIPALBINDING_LIST") + PermissionWorkspacePrincipalBindingSave = WorkspacePermissions.Entry("WORKSPACEPRINCIPALBINDING_SAVE") + + PermissionWorkspaceUserGroupList = WorkspacePermissions.Entry("WORKSPACEUSERGROUP_LIST") + + PermissionWorkspaceDelete = WorkspacePermissions.Entry("WORKSPACE_DELETE") + PermissionWorkspaceList = WorkspacePermissions.Entry("WORKSPACE_LIST") + PermissionWorkspaceSave = WorkspacePermissions.Entry("WORKSPACE_SAVE") +) diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go index 4ba8623..02511e0 100644 --- a/buildingblock_definition_version.go +++ b/buildingblock_definition_version.go @@ -156,6 +156,7 @@ type MeshBuildingBlockDefinitionVersionSpec struct { BuildingBlockDefinitionRef *BuildingBlockDefinitionRef `json:"buildingBlockDefinitionRef" tfsdk:"-"` OnlyApplyOncePerTenant bool `json:"onlyApplyOncePerTenant" tfsdk:"only_apply_once_per_tenant"` DeletionMode BuildingBlockDeletionMode `json:"deletionMode" tfsdk:"deletion_mode"` + Permissions []ApiPermission `json:"permissions,omitempty" tfsdk:"permissions"` Outputs map[string]MeshBuildingBlockDefinitionOutput `json:"outputs" tfsdk:"outputs"` VersionNumber *int64 `json:"versionNumber,omitempty" tfsdk:"version_number"` State *MeshBuildingBlockDefinitionVersionState `json:"state,omitempty" tfsdk:"state"` From 25726ac7078bf4acdf57d5653ffe996c5953c176 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Tue, 17 Feb 2026 13:50:03 +0100 Subject: [PATCH 109/200] fix: missing/wrong azure config fields --- platform_config_azure.go | 1 + 1 file changed, 1 insertion(+) diff --git a/platform_config_azure.go b/platform_config_azure.go index c805a2e..f3c411e 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -10,6 +10,7 @@ type AzurePlatformConfig struct { type AzureReplicationConfig struct { ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + UpdateSubscriptionName bool `json:"updateSubscriptionName" tfsdk:"update_subscription_name"` Provisioning *AzureSubscriptionProvisioningConfig `json:"provisioning,omitempty" tfsdk:"provisioning"` B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` SubscriptionNamePattern string `json:"subscriptionNamePattern" tfsdk:"subscription_name_pattern"` From 92797c7aa56097c656c1d4d8911c653c663059e6 Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Tue, 17 Feb 2026 11:07:26 +0100 Subject: [PATCH 110/200] feat: add parameter support to ServiceInstance resource --- service_instance.go | 23 +++++++++++++++-------- types/clienttypes.go | 2 ++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/service_instance.go b/service_instance.go index 58b6f2f..3312b61 100644 --- a/service_instance.go +++ b/service_instance.go @@ -4,6 +4,7 @@ import ( "context" "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types" ) type MeshServiceInstance struct { @@ -21,13 +22,19 @@ type MeshServiceInstanceMetadata struct { } type MeshServiceInstanceSpec struct { - Creator string `json:"creator" tfsdk:"creator"` - DisplayName string `json:"displayName" tfsdk:"display_name"` - PlanId string `json:"planId" tfsdk:"plan_id"` - ServiceId string `json:"serviceId" tfsdk:"service_id"` + Creator string `json:"creator" tfsdk:"creator"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + PlanId string `json:"planId" tfsdk:"plan_id"` + ServiceId string `json:"serviceId" tfsdk:"service_id"` + Parameters map[string]types.Any `json:"parameters" tfsdk:"parameters"` } -type MeshServiceInstanceClient struct { +type MeshServiceInstanceClient interface { + Read(ctx context.Context, instanceId string) (*MeshServiceInstance, error) + List(ctx context.Context, filter *MeshServiceInstanceFilter) ([]MeshServiceInstance, error) +} + +type meshServiceInstanceClient struct { meshObject internal.MeshObjectClient[MeshServiceInstance] } @@ -40,14 +47,14 @@ type MeshServiceInstanceFilter struct { } func newServiceInstanceClient(ctx context.Context, httpClient *internal.HttpClient) MeshServiceInstanceClient { - return MeshServiceInstanceClient{internal.NewMeshObjectClient[MeshServiceInstance](ctx, httpClient, "v2")} + return meshServiceInstanceClient{internal.NewMeshObjectClient[MeshServiceInstance](ctx, httpClient, "v2")} } -func (c MeshServiceInstanceClient) Read(ctx context.Context, instanceId string) (*MeshServiceInstance, error) { +func (c meshServiceInstanceClient) Read(ctx context.Context, instanceId string) (*MeshServiceInstance, error) { return c.meshObject.Get(ctx, instanceId) } -func (c MeshServiceInstanceClient) List(ctx context.Context, filter *MeshServiceInstanceFilter) ([]MeshServiceInstance, error) { +func (c meshServiceInstanceClient) List(ctx context.Context, filter *MeshServiceInstanceFilter) ([]MeshServiceInstance, error) { var options []internal.RequestOption if filter != nil { if filter.WorkspaceIdentifier != nil { diff --git a/types/clienttypes.go b/types/clienttypes.go index 50c7a87..f1415cb 100644 --- a/types/clienttypes.go +++ b/types/clienttypes.go @@ -13,4 +13,6 @@ type ( } SecretOrAny = variant.Variant[Secret, any] + + Any any ) From 79b2afda6fd6014cdff7171ccd792d55462db961 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 17 Feb 2026 20:32:41 +0100 Subject: [PATCH 111/200] fix: make BBD notification_subscribers a set and handle removal of invalid usernames by backend --- buildingblock_definition.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/buildingblock_definition.go b/buildingblock_definition.go index 4323804..b601eae 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -4,6 +4,7 @@ import ( "context" "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types" "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) @@ -33,8 +34,8 @@ type MeshBuildingBlockDefinitionSpec struct { SupportURL *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationURL *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` // NotificationSubscribers can also specify emails with prefix 'email:', so it's not only usernames (as the JSON field name suggests)! - NotificationSubscribers []string `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` - Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` + NotificationSubscribers []types.SetElem `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` + Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` // SupportedPlatforms are currently platform types only. Specifying single platforms is currently unsupported. // Have this list of string with a dedicated type, to convert it to/from Platform Type refs. SupportedPlatforms []BuildingBlockDefinitionSupportedPlatform `json:"supportedPlatforms" tfsdk:"supported_platforms"` From 3edb184c7236a2a57a47592c656738784203ac1c Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Tue, 17 Feb 2026 12:28:14 +0100 Subject: [PATCH 112/200] feat: add `owned_by_workspace` for meshstack_location CU-86c88kv75 --- client.go | 2 +- location.go | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index e47a1f4..dce111f 100644 --- a/client.go +++ b/client.go @@ -14,7 +14,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.2.0") +var MinMeshStackVersion = version.MustParse("2026.7.0") type Client struct { BuildingBlock MeshBuildingBlockClient diff --git a/location.go b/location.go index 13bbee1..5bcbb20 100644 --- a/location.go +++ b/location.go @@ -14,8 +14,9 @@ type MeshLocation struct { } type MeshLocationMetadata struct { - Name string `json:"name" tfsdk:"name"` - Uuid string `json:"uuid" tfsdk:"uuid"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Uuid string `json:"uuid" tfsdk:"uuid"` } type MeshLocationSpec struct { @@ -34,7 +35,8 @@ type MeshLocationCreate struct { } type MeshLocationCreateMetadata struct { - Name string `json:"name" tfsdk:"name"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } type MeshLocationClient interface { From 09bfce5039a936f7b4b36de3d83d3c98929d4fea Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Mon, 16 Feb 2026 11:55:25 +0100 Subject: [PATCH 113/200] feat: make meshPlatform related resources GA --- integration.go | 2 +- landingzone.go | 2 +- location.go | 2 +- platform.go | 2 +- platform_type.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration.go b/integration.go index 80e583a..5983c75 100644 --- a/integration.go +++ b/integration.go @@ -59,7 +59,7 @@ type meshIntegrationClientImpl struct { } func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) MeshIntegrationClient { - return &meshIntegrationClientImpl{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1-preview")} + return &meshIntegrationClientImpl{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1")} } func (c meshIntegrationClientImpl) Create(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) { diff --git a/landingzone.go b/landingzone.go index 0deb06d..4254433 100644 --- a/landingzone.go +++ b/landingzone.go @@ -71,7 +71,7 @@ type MeshLandingZoneClient struct { } func newLandingZoneClient(ctx context.Context, httpClient *internal.HttpClient) MeshLandingZoneClient { - return MeshLandingZoneClient{internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1-preview")} + return MeshLandingZoneClient{internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1")} } func (c MeshLandingZoneClient) Read(ctx context.Context, name string) (*MeshLandingZone, error) { diff --git a/location.go b/location.go index 5bcbb20..57ab04c 100644 --- a/location.go +++ b/location.go @@ -51,7 +51,7 @@ type meshLocationClient struct { } func newLocationClient(ctx context.Context, httpClient *internal.HttpClient) MeshLocationClient { - return meshLocationClient{internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1-preview")} + return meshLocationClient{internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1")} } func (c meshLocationClient) Read(ctx context.Context, name string) (*MeshLocation, error) { diff --git a/platform.go b/platform.go index 34a7d89..4686d95 100644 --- a/platform.go +++ b/platform.go @@ -115,7 +115,7 @@ type meshPlatformClient struct { } func newPlatformClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformClient { - return meshPlatformClient{internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2-preview")} + return meshPlatformClient{internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2")} } func (c meshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatform, error) { diff --git a/platform_type.go b/platform_type.go index 3e93e09..c97024b 100644 --- a/platform_type.go +++ b/platform_type.go @@ -60,7 +60,7 @@ type meshPlatformTypeClient struct { } func newPlatformTypeClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformTypeClient { - return meshPlatformTypeClient{internal.NewMeshObjectClient[MeshPlatformType](ctx, httpClient, "v1-preview")} + return meshPlatformTypeClient{internal.NewMeshObjectClient[MeshPlatformType](ctx, httpClient, "v1")} } func (c meshPlatformTypeClient) Create(ctx context.Context, platformType *MeshPlatformTypeCreate) (*MeshPlatformType, error) { From cb7aa2384ed211c8f879b26678dd1f210580998d Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Wed, 18 Feb 2026 10:14:45 +0100 Subject: [PATCH 114/200] refactor: remove apiVersion and kind from platform related terraform models --- landingzone.go | 6 +++--- location.go | 4 ++-- platform.go | 8 ++++---- platform_type.go | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/landingzone.go b/landingzone.go index 4254433..1e8e33e 100644 --- a/landingzone.go +++ b/landingzone.go @@ -7,8 +7,8 @@ import ( ) type MeshLandingZone struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` + Kind string `json:"kind" tfsdk:"-"` Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` Status MeshLandingZoneStatus `json:"status" tfsdk:"status"` @@ -61,7 +61,7 @@ type MeshLandingZoneQuota struct { } type MeshLandingZoneCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` } diff --git a/location.go b/location.go index 57ab04c..9a25b33 100644 --- a/location.go +++ b/location.go @@ -7,7 +7,7 @@ import ( ) type MeshLocation struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` Metadata MeshLocationMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` Status MeshLocationStatus `json:"status" tfsdk:"status"` @@ -29,7 +29,7 @@ type MeshLocationStatus struct { } type MeshLocationCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` Metadata MeshLocationCreateMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` } diff --git a/platform.go b/platform.go index 4686d95..ed42e96 100644 --- a/platform.go +++ b/platform.go @@ -7,8 +7,8 @@ import ( ) type MeshPlatform struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` + Kind string `json:"kind" tfsdk:"-"` Metadata MeshPlatformMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` } @@ -66,7 +66,7 @@ type PlatformConfig struct { } type MeshPlatformCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` Metadata MeshPlatformCreateMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` } @@ -77,7 +77,7 @@ type MeshPlatformCreateMetadata struct { } type MeshPlatformUpdate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` Metadata MeshPlatformUpdateMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` } diff --git a/platform_type.go b/platform_type.go index c97024b..29df7a2 100644 --- a/platform_type.go +++ b/platform_type.go @@ -7,8 +7,8 @@ import ( ) type MeshPlatformType struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` + Kind string `json:"kind" tfsdk:"-"` Metadata MeshPlatformTypeMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` Status MeshPlatformTypeStatus `json:"status" tfsdk:"status"` @@ -36,8 +36,8 @@ type MeshPlatformTypeSpec struct { } type MeshPlatformTypeCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` + ApiVersion string `json:"apiVersion" tfsdk:"-"` + Kind string `json:"kind" tfsdk:"-"` Metadata MeshPlatformTypeCreateMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` } From 415725fcc059fa8683a65e885dc2bbc85e382d41 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 18 Feb 2026 19:40:51 +0100 Subject: [PATCH 115/200] feat: support write-only ephemeral secrets in meshstack_platform resource --- platform.go | 74 +++++++++++++++------------------------- platform_config_azure.go | 2 +- 2 files changed, 29 insertions(+), 47 deletions(-) diff --git a/platform.go b/platform.go index ed42e96..2393c97 100644 --- a/platform.go +++ b/platform.go @@ -4,6 +4,7 @@ import ( "context" "github.com/meshcloud/terraform-provider-meshstack/client/internal" + clientTypes "github.com/meshcloud/terraform-provider-meshstack/client/types" ) type MeshPlatform struct { @@ -14,30 +15,30 @@ type MeshPlatform struct { } type MeshPlatformMetadata struct { - Name string `json:"name" tfsdk:"name"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - Uuid string `json:"uuid" tfsdk:"uuid"` + Name string `json:"name" tfsdk:"name"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` } type MeshPlatformSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - Endpoint string `json:"endpoint" tfsdk:"endpoint"` - SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` - DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` - LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` - ContributingWorkspaces []string `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` - Availability PlatformAvailability `json:"availability" tfsdk:"availability"` - Config PlatformConfig `json:"config" tfsdk:"config"` - QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + Endpoint string `json:"endpoint" tfsdk:"endpoint"` + SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` + DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` + ContributingWorkspaces []clientTypes.SetElem `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` + Availability PlatformAvailability `json:"availability" tfsdk:"availability"` + Config PlatformConfig `json:"config" tfsdk:"config"` + QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` } type QuotaDefinition struct { QuotaKey string `json:"quotaKey" tfsdk:"quota_key"` - MinValue int `json:"minValue" tfsdk:"min_value"` - MaxValue int `json:"maxValue" tfsdk:"max_value"` + MinValue int64 `json:"minValue" tfsdk:"min_value"` + MaxValue int64 `json:"maxValue" tfsdk:"max_value"` Unit string `json:"unit" tfsdk:"unit"` - AutoApprovalThreshold int `json:"autoApprovalThreshold" tfsdk:"auto_approval_threshold"` + AutoApprovalThreshold int64 `json:"autoApprovalThreshold" tfsdk:"auto_approval_threshold"` Description string `json:"description" tfsdk:"description"` Label string `json:"label" tfsdk:"label"` } @@ -48,9 +49,9 @@ type LocationRef struct { } type PlatformAvailability struct { - Restriction string `json:"restriction" tfsdk:"restriction"` - PublicationState string `json:"publicationState" tfsdk:"publication_state"` - RestrictedToWorkspaces []string `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` + Restriction string `json:"restriction" tfsdk:"restriction"` + PublicationState string `json:"publicationState" tfsdk:"publication_state"` + RestrictedToWorkspaces []clientTypes.SetElem `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` } type PlatformConfig struct { @@ -65,29 +66,6 @@ type PlatformConfig struct { OpenShift *OpenShiftPlatformConfig `json:"openshift,omitempty" tfsdk:"openshift"` } -type MeshPlatformCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Metadata MeshPlatformCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` -} - -type MeshPlatformCreateMetadata struct { - Name string `json:"name" tfsdk:"name"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` -} - -type MeshPlatformUpdate struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Metadata MeshPlatformUpdateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` -} - -type MeshPlatformUpdateMetadata struct { - Name string `json:"name" tfsdk:"name"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - Uuid string `json:"uuid" tfsdk:"uuid"` -} - type MeshPlatformMeteringProcessingConfig struct { CompactTimelinesAfterDays int64 `json:"compactTimelinesAfterDays" tfsdk:"compact_timelines_after_days"` DeleteRawDataAfterDays int64 `json:"deleteRawDataAfterDays" tfsdk:"delete_raw_data_after_days"` @@ -105,8 +83,8 @@ type TagMapper struct { type MeshPlatformClient interface { Read(ctx context.Context, uuid string) (*MeshPlatform, error) - Create(ctx context.Context, platform *MeshPlatformCreate) (*MeshPlatform, error) - Update(ctx context.Context, uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) + Create(ctx context.Context, platform MeshPlatform) (*MeshPlatform, error) + Update(ctx context.Context, uuid string, platform MeshPlatform) (*MeshPlatform, error) Delete(ctx context.Context, uuid string) error } @@ -122,11 +100,15 @@ func (c meshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatfor return c.meshObject.Get(ctx, uuid) } -func (c meshPlatformClient) Create(ctx context.Context, platform *MeshPlatformCreate) (*MeshPlatform, error) { +func (c meshPlatformClient) Create(ctx context.Context, platform MeshPlatform) (*MeshPlatform, error) { + platform.Kind = c.meshObject.Kind + platform.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Post(ctx, platform) } -func (c meshPlatformClient) Update(ctx context.Context, uuid string, platform *MeshPlatformUpdate) (*MeshPlatform, error) { +func (c meshPlatformClient) Update(ctx context.Context, uuid string, platform MeshPlatform) (*MeshPlatform, error) { + platform.Kind = c.meshObject.Kind + platform.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Put(ctx, uuid, platform) } diff --git a/platform_config_azure.go b/platform_config_azure.go index f3c411e..30c9620 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -42,7 +42,7 @@ type AzureGraphApiCredentials struct { } type AzureSubscriptionProvisioningConfig struct { - SubscriptionOwnerObjectIds []string `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` + SubscriptionOwnerObjectIds []types.SetElem `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` EnterpriseEnrollment *AzureEnterpriseEnrollmentConfig `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` CustomerAgreement *AzureCustomerAgreementConfig `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` PreProvisioned *AzurePreProvisionedSubscriptionConfig `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` From 7b0b05712ca79da76922a5607a9a7b2a671f6470 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 19 Feb 2026 11:58:45 +0100 Subject: [PATCH 116/200] refactor: rename to StringSetElem --- buildingblock_definition.go | 4 ++-- buildingblock_definition_version.go | 14 +++++++------- platform.go | 26 +++++++++++++------------- platform_config_azure.go | 2 +- types/clienttypes.go | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/buildingblock_definition.go b/buildingblock_definition.go index b601eae..124a012 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -34,8 +34,8 @@ type MeshBuildingBlockDefinitionSpec struct { SupportURL *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationURL *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` // NotificationSubscribers can also specify emails with prefix 'email:', so it's not only usernames (as the JSON field name suggests)! - NotificationSubscribers []types.SetElem `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` - Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` + NotificationSubscribers []types.StringSetElem `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` + Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` // SupportedPlatforms are currently platform types only. Specifying single platforms is currently unsupported. // Have this list of string with a dedicated type, to convert it to/from Platform Type refs. SupportedPlatforms []BuildingBlockDefinitionSupportedPlatform `json:"supportedPlatforms" tfsdk:"supported_platforms"` diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go index 02511e0..015da2f 100644 --- a/buildingblock_definition_version.go +++ b/buildingblock_definition_version.go @@ -98,13 +98,13 @@ type MeshBuildingBlockDefinitionInput struct { // Otherwise, the [types.Variant] is of [types.Any] (case [types.Variant.Y]). // As this is a fallback detection when JSON (un)marshaling, // types.Any must go second as [types.Variant] intentionally prefers X over Y. - Argument types.SecretOrAny `json:"argument,omitempty" tfsdk:"argument"` - DefaultValue types.SecretOrAny `json:"defaultValue,omitempty" tfsdk:"default_value"` - UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` - SelectableValues []types.SetElem `json:"selectableValues,omitempty" tfsdk:"selectable_values"` - Description *string `json:"description,omitempty" tfsdk:"description"` - ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` - ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` + Argument types.SecretOrAny `json:"argument,omitempty" tfsdk:"argument"` + DefaultValue types.SecretOrAny `json:"defaultValue,omitempty" tfsdk:"default_value"` + UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` + SelectableValues []types.StringSetElem `json:"selectableValues,omitempty" tfsdk:"selectable_values"` + Description *string `json:"description,omitempty" tfsdk:"description"` + ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` + ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` } func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSON(bytes []byte) error { diff --git a/platform.go b/platform.go index 2393c97..d4f3027 100644 --- a/platform.go +++ b/platform.go @@ -21,16 +21,16 @@ type MeshPlatformMetadata struct { } type MeshPlatformSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - Endpoint string `json:"endpoint" tfsdk:"endpoint"` - SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` - DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` - LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` - ContributingWorkspaces []clientTypes.SetElem `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` - Availability PlatformAvailability `json:"availability" tfsdk:"availability"` - Config PlatformConfig `json:"config" tfsdk:"config"` - QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + Endpoint string `json:"endpoint" tfsdk:"endpoint"` + SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` + DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` + ContributingWorkspaces []clientTypes.StringSetElem `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` + Availability PlatformAvailability `json:"availability" tfsdk:"availability"` + Config PlatformConfig `json:"config" tfsdk:"config"` + QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` } type QuotaDefinition struct { @@ -49,9 +49,9 @@ type LocationRef struct { } type PlatformAvailability struct { - Restriction string `json:"restriction" tfsdk:"restriction"` - PublicationState string `json:"publicationState" tfsdk:"publication_state"` - RestrictedToWorkspaces []clientTypes.SetElem `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` + Restriction string `json:"restriction" tfsdk:"restriction"` + PublicationState string `json:"publicationState" tfsdk:"publication_state"` + RestrictedToWorkspaces []clientTypes.StringSetElem `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` } type PlatformConfig struct { diff --git a/platform_config_azure.go b/platform_config_azure.go index 30c9620..10c046e 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -42,7 +42,7 @@ type AzureGraphApiCredentials struct { } type AzureSubscriptionProvisioningConfig struct { - SubscriptionOwnerObjectIds []types.SetElem `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` + SubscriptionOwnerObjectIds []types.StringSetElem `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` EnterpriseEnrollment *AzureEnterpriseEnrollmentConfig `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` CustomerAgreement *AzureCustomerAgreementConfig `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` PreProvisioned *AzurePreProvisionedSubscriptionConfig `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` diff --git a/types/clienttypes.go b/types/clienttypes.go index f1415cb..957a3bb 100644 --- a/types/clienttypes.go +++ b/types/clienttypes.go @@ -5,7 +5,7 @@ import ( ) type ( - SetElem string + StringSetElem string Secret struct { Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` From a54b425927173eff97b56d741610776c5cf66897 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 24 Feb 2026 11:51:14 +0100 Subject: [PATCH 117/200] fix: properly handle null/empty in BBD, simplify set handling in generic.ValueFrom --- buildingblock_definition.go | 6 +++--- buildingblock_definition_version.go | 18 ++++++++-------- platform.go | 32 ++++++++++++++--------------- platform_config_azure.go | 4 ++-- platform_config_openshift.go | 16 ++++++++------- types/clienttypes.go | 24 +++++++++++++++++++++- types/clienttypes_test.go | 30 +++++++++++++++++++++++++++ 7 files changed, 92 insertions(+), 38 deletions(-) diff --git a/buildingblock_definition.go b/buildingblock_definition.go index 124a012..605bbd2 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -34,11 +34,11 @@ type MeshBuildingBlockDefinitionSpec struct { SupportURL *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationURL *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` // NotificationSubscribers can also specify emails with prefix 'email:', so it's not only usernames (as the JSON field name suggests)! - NotificationSubscribers []types.StringSetElem `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` - Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` + NotificationSubscribers types.Set[string] `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` + Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` // SupportedPlatforms are currently platform types only. Specifying single platforms is currently unsupported. // Have this list of string with a dedicated type, to convert it to/from Platform Type refs. - SupportedPlatforms []BuildingBlockDefinitionSupportedPlatform `json:"supportedPlatforms" tfsdk:"supported_platforms"` + SupportedPlatforms types.Set[BuildingBlockDefinitionSupportedPlatform] `json:"supportedPlatforms" tfsdk:"supported_platforms"` } type MeshBuildingBlockDefinitionStatusVersion struct { diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go index 015da2f..dd3d627 100644 --- a/buildingblock_definition_version.go +++ b/buildingblock_definition_version.go @@ -98,13 +98,13 @@ type MeshBuildingBlockDefinitionInput struct { // Otherwise, the [types.Variant] is of [types.Any] (case [types.Variant.Y]). // As this is a fallback detection when JSON (un)marshaling, // types.Any must go second as [types.Variant] intentionally prefers X over Y. - Argument types.SecretOrAny `json:"argument,omitempty" tfsdk:"argument"` - DefaultValue types.SecretOrAny `json:"defaultValue,omitempty" tfsdk:"default_value"` - UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` - SelectableValues []types.StringSetElem `json:"selectableValues,omitempty" tfsdk:"selectable_values"` - Description *string `json:"description,omitempty" tfsdk:"description"` - ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` - ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` + Argument types.SecretOrAny `json:"argument,omitempty" tfsdk:"argument"` + DefaultValue types.SecretOrAny `json:"defaultValue,omitempty" tfsdk:"default_value"` + UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` + SelectableValues types.Set[string] `json:"selectableValues,omitempty" tfsdk:"selectable_values"` + Description *string `json:"description,omitempty" tfsdk:"description"` + ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` + ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` } func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSON(bytes []byte) error { @@ -156,12 +156,12 @@ type MeshBuildingBlockDefinitionVersionSpec struct { BuildingBlockDefinitionRef *BuildingBlockDefinitionRef `json:"buildingBlockDefinitionRef" tfsdk:"-"` OnlyApplyOncePerTenant bool `json:"onlyApplyOncePerTenant" tfsdk:"only_apply_once_per_tenant"` DeletionMode BuildingBlockDeletionMode `json:"deletionMode" tfsdk:"deletion_mode"` - Permissions []ApiPermission `json:"permissions,omitempty" tfsdk:"permissions"` + Permissions types.Set[ApiPermission] `json:"permissions,omitempty" tfsdk:"permissions"` Outputs map[string]MeshBuildingBlockDefinitionOutput `json:"outputs" tfsdk:"outputs"` VersionNumber *int64 `json:"versionNumber,omitempty" tfsdk:"version_number"` State *MeshBuildingBlockDefinitionVersionState `json:"state,omitempty" tfsdk:"state"` RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` - DependencyDefinitionUUIDs []BuildingBlockDependencyRef `json:"dependencyDefinitionUuids,omitempty" tfsdk:"dependency_refs"` + DependencyDefinitionUUIDs types.Set[BuildingBlockDependencyRef] `json:"dependencyDefinitionUuids,omitempty" tfsdk:"dependency_refs"` Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` } diff --git a/platform.go b/platform.go index d4f3027..cc7133c 100644 --- a/platform.go +++ b/platform.go @@ -4,7 +4,7 @@ import ( "context" "github.com/meshcloud/terraform-provider-meshstack/client/internal" - clientTypes "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types" ) type MeshPlatform struct { @@ -21,16 +21,16 @@ type MeshPlatformMetadata struct { } type MeshPlatformSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - Endpoint string `json:"endpoint" tfsdk:"endpoint"` - SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` - DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` - LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` - ContributingWorkspaces []clientTypes.StringSetElem `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` - Availability PlatformAvailability `json:"availability" tfsdk:"availability"` - Config PlatformConfig `json:"config" tfsdk:"config"` - QuotaDefinitions []QuotaDefinition `json:"quotaDefinitions" tfsdk:"quota_definitions"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + Endpoint string `json:"endpoint" tfsdk:"endpoint"` + SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` + DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` + ContributingWorkspaces types.Set[string] `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` + Availability PlatformAvailability `json:"availability" tfsdk:"availability"` + Config PlatformConfig `json:"config" tfsdk:"config"` + QuotaDefinitions types.Set[QuotaDefinition] `json:"quotaDefinitions" tfsdk:"quota_definitions"` } type QuotaDefinition struct { @@ -49,9 +49,9 @@ type LocationRef struct { } type PlatformAvailability struct { - Restriction string `json:"restriction" tfsdk:"restriction"` - PublicationState string `json:"publicationState" tfsdk:"publication_state"` - RestrictedToWorkspaces []clientTypes.StringSetElem `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` + Restriction string `json:"restriction" tfsdk:"restriction"` + PublicationState string `json:"publicationState" tfsdk:"publication_state"` + RestrictedToWorkspaces types.Set[string] `json:"restrictedToWorkspaces,omitempty" tfsdk:"restricted_to_workspaces"` } type PlatformConfig struct { @@ -72,8 +72,8 @@ type MeshPlatformMeteringProcessingConfig struct { } type MeshTenantTags struct { - NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` - TagMappers []TagMapper `json:"tagMappers" tfsdk:"tag_mappers"` + NamespacePrefix string `json:"namespacePrefix" tfsdk:"namespace_prefix"` + TagMappers types.Set[TagMapper] `json:"tagMappers" tfsdk:"tag_mappers"` } type TagMapper struct { diff --git a/platform_config_azure.go b/platform_config_azure.go index 10c046e..966e1bf 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -17,7 +17,7 @@ type AzureReplicationConfig struct { GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` BlueprintServicePrincipal string `json:"blueprintServicePrincipal" tfsdk:"blueprint_service_principal"` BlueprintLocation string `json:"blueprintLocation" tfsdk:"blueprint_location"` - AzureRoleMappings []AzureRoleMapping `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` + AzureRoleMappings types.Set[AzureRoleMapping] `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` @@ -42,7 +42,7 @@ type AzureGraphApiCredentials struct { } type AzureSubscriptionProvisioningConfig struct { - SubscriptionOwnerObjectIds []types.StringSetElem `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` + SubscriptionOwnerObjectIds types.Set[string] `json:"subscriptionOwnerObjectIds" tfsdk:"subscription_owner_object_ids"` EnterpriseEnrollment *AzureEnterpriseEnrollmentConfig `json:"enterpriseEnrollment,omitempty" tfsdk:"enterprise_enrollment"` CustomerAgreement *AzureCustomerAgreementConfig `json:"customerAgreement,omitempty" tfsdk:"customer_agreement"` PreProvisioned *AzurePreProvisionedSubscriptionConfig `json:"preProvisioned,omitempty" tfsdk:"pre_provisioned"` diff --git a/platform_config_openshift.go b/platform_config_openshift.go index 1dcf722..e974b95 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -1,5 +1,7 @@ package client +import "github.com/meshcloud/terraform-provider-meshstack/client/types" + type OpenShiftPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` DisableSslValidation bool `json:"disableSslValidation" tfsdk:"disable_ssl_validation"` @@ -8,13 +10,13 @@ type OpenShiftPlatformConfig struct { } type OpenShiftReplicationConfig struct { - ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` - WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` - ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` - EnableTemplateInstantiation bool `json:"enableTemplateInstantiation" tfsdk:"enable_template_instantiation"` - OpenshiftRoleMappings []OpenShiftPlatformRoleMapping `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` - IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` - TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` + WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` + ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` + EnableTemplateInstantiation bool `json:"enableTemplateInstantiation" tfsdk:"enable_template_instantiation"` + OpenshiftRoleMappings types.Set[OpenShiftPlatformRoleMapping] `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` + IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` } type OpenShiftMeteringConfig struct { diff --git a/types/clienttypes.go b/types/clienttypes.go index 957a3bb..17589cb 100644 --- a/types/clienttypes.go +++ b/types/clienttypes.go @@ -1,11 +1,14 @@ package types import ( + "reflect" + "strings" + "github.com/meshcloud/terraform-provider-meshstack/client/types/variant" ) type ( - StringSetElem string + Set[T any] []T Secret struct { Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` @@ -16,3 +19,22 @@ type ( Any any ) + +// IsSet returns true if the given type uses the generic Set type, ignoring the concrete container type T. +func IsSet(other reflect.Type) bool { + var ( + setType = reflect.TypeFor[Set[any]]() + ) + if other.PkgPath() == setType.PkgPath() { + stripGenerics := func(s string) string { + if startIdx := strings.Index(s, "["); startIdx > 0 { + return s[0 : startIdx-1] + } + return s + } + if stripGenerics(other.Name()) == stripGenerics(setType.Name()) { + return true + } + } + return false +} diff --git a/types/clienttypes_test.go b/types/clienttypes_test.go index cfaeb95..8c4e7f3 100644 --- a/types/clienttypes_test.go +++ b/types/clienttypes_test.go @@ -2,6 +2,7 @@ package types import ( "encoding/json" + "reflect" "testing" "github.com/stretchr/testify/assert" @@ -45,3 +46,32 @@ func TestSecretOrAny(t *testing.T) { }) } } + +func TestIsSet(t *testing.T) { + type ( + someStruct struct { + A string + } + someString string + someSet Set[someString] + ) + tests := []struct { + name string + t reflect.Type + want bool + }{ + {"bool", reflect.TypeFor[bool](), false}, + {"any", reflect.TypeFor[any](), false}, + {"int", reflect.TypeFor[any](), false}, + {"some set (not supported)", reflect.TypeFor[someSet](), false}, + {"set of string", reflect.TypeFor[Set[string]](), true}, + {"set of int", reflect.TypeFor[Set[string]](), true}, + {"set of struct", reflect.TypeFor[Set[someStruct]](), true}, + {"set of some string", reflect.TypeFor[Set[someString]](), true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, IsSet(tt.t), "IsSet(%v)", tt.t) + }) + } +} From 76f4f65c34e1149321742650f6c31918a544e40c Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 9 Mar 2026 22:22:01 +0100 Subject: [PATCH 118/200] feat: add pre_run_script field to meshstack_building_block_definition and update examples --- buildingblock_definition_version_implementation.go | 1 + 1 file changed, 1 insertion(+) diff --git a/buildingblock_definition_version_implementation.go b/buildingblock_definition_version_implementation.go index f8d169d..9b67e9f 100644 --- a/buildingblock_definition_version_implementation.go +++ b/buildingblock_definition_version_implementation.go @@ -35,6 +35,7 @@ type MeshBuildingBlockDefinitionTerraformImplementation struct { SSHKnownHost *MeshBuildingBlockDefinitionSshKnownHost `json:"sshKnownHost,omitempty" tfsdk:"ssh_known_host"` UseMeshHTTPBackendFallback bool `json:"useMeshHttpBackendFallback" tfsdk:"use_mesh_http_backend_fallback"` SSHPrivateKey *types.Secret `json:"sshPrivateKey,omitempty" tfsdk:"ssh_private_key"` + PreRunScript *string `json:"preRunScript,omitempty" tfsdk:"pre_run_script"` } type MeshBuildingBlockDefinitionGitHubWorkflowsImplementation struct { From 65c842a2e05bcc1a5d5edb3f92afde6259f65083 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:04:34 +0000 Subject: [PATCH 119/200] fix: address review comments and CI failures for pre_run_script field Co-authored-by: JohannesRudolph <130103+JohannesRudolph@users.noreply.github.com> --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index dce111f..a3c8eef 100644 --- a/client.go +++ b/client.go @@ -14,7 +14,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.7.0") +var MinMeshStackVersion = version.MustParse("2026.10.0") type Client struct { BuildingBlock MeshBuildingBlockClient From 35e74a5f9c7222360595cbb6b057a8f7f57a1538 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:54:41 +0000 Subject: [PATCH 120/200] feat: Add aws_identity_store support to meshstack_platform resource Co-authored-by: JohannesRudolph <130103+JohannesRudolph@users.noreply.github.com> --- platform_config_aws.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/platform_config_aws.go b/platform_config_aws.go index 7bfe596..e1b96f3 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -19,6 +19,7 @@ type AwsReplicationConfig struct { AccountEmailPattern string `json:"accountEmailPattern" tfsdk:"account_email_pattern"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` AwsSso *AwsSsoConfig `json:"awsSso,omitempty" tfsdk:"aws_sso"` + AwsIdentityStore *AwsIdentityStoreConfig `json:"awsIdentityStore,omitempty" tfsdk:"aws_identity_store"` EnrollmentConfiguration *AwsEnrollmentConfiguration `json:"enrollmentConfiguration,omitempty" tfsdk:"enrollment_configuration"` SelfDowngradeAccessRole bool `json:"selfDowngradeAccessRole" tfsdk:"self_downgrade_access_role"` SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` @@ -66,6 +67,24 @@ type AwsEnrollmentConfiguration struct { AccountFactoryProductId string `json:"accountFactoryProductId" tfsdk:"account_factory_product_id"` } +type AwsIdentityStoreConfig struct { + IdentityStoreId string `json:"identityStoreId" tfsdk:"identity_store_id"` + Arn string `json:"arn" tfsdk:"arn"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + AwsRoleMappings []AwsIdentityStoreRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` + SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` +} + +type AwsIdentityStoreRoleMapping struct { + ProjectRoleRef AwsIdentityStoreProjectRoleRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + AwsRole string `json:"awsRole" tfsdk:"aws_role"` + PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` +} + +type AwsIdentityStoreProjectRoleRef struct { + Name string `json:"name" tfsdk:"name"` +} + type AwsMeteringConfig struct { AccessConfig AwsAccessConfig `json:"accessConfig" tfsdk:"access_config"` Filter string `json:"filter" tfsdk:"filter"` From c6d89618a4ac00d375dfbbc87585683fac8103ee Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 12 Mar 2026 09:22:39 +0100 Subject: [PATCH 121/200] fix: use existing role refs and remove wrong mst- prefix docs meshStack does not enforce such a prefix, a parallel fix will be made to meshStack docs upstream --- platform_config_aws.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/platform_config_aws.go b/platform_config_aws.go index e1b96f3..b7a60db 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -76,13 +76,9 @@ type AwsIdentityStoreConfig struct { } type AwsIdentityStoreRoleMapping struct { - ProjectRoleRef AwsIdentityStoreProjectRoleRef `json:"projectRoleRef" tfsdk:"project_role_ref"` - AwsRole string `json:"awsRole" tfsdk:"aws_role"` - PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` -} - -type AwsIdentityStoreProjectRoleRef struct { - Name string `json:"name" tfsdk:"name"` + ProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + AwsRole string `json:"awsRole" tfsdk:"aws_role"` + PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` } type AwsMeteringConfig struct { From 807b3b2823de9f7d7cf74e4bf66930c8d90c0dd4 Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Wed, 18 Mar 2026 10:26:00 +0100 Subject: [PATCH 122/200] fix: only support the actually valid output IO types BD-2293 --- buildingblock_definition_version.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go index dd3d627..0c300b4 100644 --- a/buildingblock_definition_version.go +++ b/buildingblock_definition_version.go @@ -43,6 +43,13 @@ var ( MeshBuildingBlockIOTypeMultiSelect = MeshBuildingBlockIOTypes.Entry("MULTI_SELECT") ) +var MeshBuildingBlockOutputIOTypes = enum.Of( + MeshBuildingBlockIOTypeString, + MeshBuildingBlockIOTypeCode, + MeshBuildingBlockIOTypeInteger, + MeshBuildingBlockIOTypeBoolean, +) + type MeshBuildingBlockInputAssignmentType string var ( From 73b7ccbf54255718341e78f4e517e482d1a54105 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 1 Apr 2026 21:46:52 +0200 Subject: [PATCH 123/200] fix: fix platform config boolean flags for AWS and AzureRG (#139) * feat: reproducer for incorrect platform flag handling * fix: remove allowHierarchicalManagementGroupAssignment from AzureRG platform config Remove the field from AzureRG resource/data source schemas, client model, example config and test expectations. This flag is only applicable to Azure (subscription-based) platforms. Fixes https://github.com/meshcloud/terraform-provider-meshstack/issues/123 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- platform_config_azurerg.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/platform_config_azurerg.go b/platform_config_azurerg.go index e984d30..dab2f73 100644 --- a/platform_config_azurerg.go +++ b/platform_config_azurerg.go @@ -6,14 +6,13 @@ type AzureRgPlatformConfig struct { } type AzureRgReplicationConfig struct { - ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` - Subscription string `json:"subscription" tfsdk:"subscription"` - ResourceGroupNamePattern string `json:"resourceGroupNamePattern" tfsdk:"resource_group_name_pattern"` - UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` - B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` - UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` - TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` - AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` - AllowHierarchicalManagementGroupAssignment bool `json:"allowHierarchicalManagementGroupAssignment" tfsdk:"allow_hierarchical_management_group_assignment"` + ServicePrincipal AzureServicePrincipalConfig `json:"servicePrincipal" tfsdk:"service_principal"` + Subscription string `json:"subscription" tfsdk:"subscription"` + ResourceGroupNamePattern string `json:"resourceGroupNamePattern" tfsdk:"resource_group_name_pattern"` + UserGroupNamePattern string `json:"userGroupNamePattern" tfsdk:"user_group_name_pattern"` + B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` + UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty" tfsdk:"administrative_unit_id"` } From 0c4bd126b82b92f7b86fb3618bd112c261d224f7 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 9 Apr 2026 16:25:49 +0200 Subject: [PATCH 124/200] refactor: remove api_version/kind, extract client interfaces BREAKING CHANGES: - Remove api_version and kind computed attributes from all resources and data sources. These were always fixed values determined by the meshObject type and are now handled internally by the client library. IMPROVEMENTS: - Extract client interfaces for all meshObject types, enabling mock-based unit testing. - Add computed 'ref' attribute to workspace, tenant_v4, and platform_type resources exposing kind + uuid for cross-resource references. - Auto-inject apiVersion/kind in HTTP layer via withMeshObjectPayload(). - Move kind inference to client/types package using InferKind[T]() generic helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- buildingblock.go | 30 ++++++++------- buildingblock_definition.go | 12 ++---- buildingblock_definition_version.go | 12 ++---- buildingblock_v2.go | 31 ++++++++------- client_kind.go | 48 ++++++++++++++++++++++++ client_kind_test.go | 33 ++++++++++++++++ integration.go | 12 ++---- internal/mesh_object_client.go | 58 +++++++++++++++++++++-------- internal/mesh_object_client_test.go | 48 ------------------------ landingzone.go | 32 +++++++++------- location.go | 12 +++--- payment_method.go | 30 ++++++++------- platform.go | 10 +---- platform_type.go | 14 +++---- project.go | 30 +++++++++------ project_binding.go | 10 ++--- project_group_binding.go | 16 +++++--- project_user_binding.go | 16 +++++--- service_instance.go | 6 +-- tag_definition.go | 6 +-- tenant.go | 24 +++++++----- tenant_v4.go | 27 ++++++++------ workspace.go | 32 +++++++++------- workspace_binding.go | 10 ++--- workspace_group_binding.go | 16 +++++--- workspace_user_binding.go | 16 +++++--- 26 files changed, 334 insertions(+), 257 deletions(-) create mode 100644 client_kind.go create mode 100644 client_kind_test.go delete mode 100644 internal/mesh_object_client_test.go diff --git a/buildingblock.go b/buildingblock.go index c89d606..97ecff7 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -18,11 +18,9 @@ const ( ) type MeshBuildingBlock struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshBuildingBlockMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` - Status MeshBuildingBlockStatus `json:"status" tfsdk:"status"` + Metadata MeshBuildingBlockMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` + Status MeshBuildingBlockStatus `json:"status" tfsdk:"status"` } type MeshBuildingBlockMetadata struct { @@ -59,10 +57,8 @@ type MeshBuildingBlockStatus struct { } type MeshBuildingBlockCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshBuildingBlockCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` + Metadata MeshBuildingBlockCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` } type MeshBuildingBlockCreateMetadata struct { @@ -76,22 +72,28 @@ type MeshBuildingBlockDefinitionRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` } -type MeshBuildingBlockClient struct { +type MeshBuildingBlockClient interface { + Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) + Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) + Delete(ctx context.Context, uuid string) error +} + +type meshBuildingBlockClient struct { meshObject internal.MeshObjectClient[MeshBuildingBlock] } func newBuildingBlockClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockClient { - return MeshBuildingBlockClient{internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1")} + return meshBuildingBlockClient{internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1")} } -func (c MeshBuildingBlockClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) { +func (c meshBuildingBlockClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) { return c.meshObject.Get(ctx, uuid) } -func (c MeshBuildingBlockClient) Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { +func (c meshBuildingBlockClient) Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { return c.meshObject.Post(ctx, bb) } -func (c MeshBuildingBlockClient) Delete(ctx context.Context, uuid string) error { +func (c meshBuildingBlockClient) Delete(ctx context.Context, uuid string) error { return c.meshObject.Delete(ctx, uuid) } diff --git a/buildingblock_definition.go b/buildingblock_definition.go index 605bbd2..0262f38 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -57,11 +57,9 @@ type MeshBuildingBlockDefinitionStatus struct { } type MeshBuildingBlockDefinition struct { - ApiVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Metadata MeshBuildingBlockDefinitionMetadata `json:"metadata"` - Spec MeshBuildingBlockDefinitionSpec `json:"spec"` - Status *MeshBuildingBlockDefinitionStatus `json:"status,omitempty"` + Metadata MeshBuildingBlockDefinitionMetadata `json:"metadata"` + Spec MeshBuildingBlockDefinitionSpec `json:"spec"` + Status *MeshBuildingBlockDefinitionStatus `json:"status,omitempty"` } type MeshBuildingBlockDefinitionClient interface { @@ -95,14 +93,10 @@ func (c meshBuildingBlockDefinitionClient) Read(ctx context.Context, uuid string } func (c meshBuildingBlockDefinitionClient) Create(ctx context.Context, definition MeshBuildingBlockDefinition) (*MeshBuildingBlockDefinition, error) { - definition.Kind = c.meshObject.Kind - definition.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Post(ctx, definition) } func (c meshBuildingBlockDefinitionClient) Update(ctx context.Context, uuid string, definition MeshBuildingBlockDefinition) (*MeshBuildingBlockDefinition, error) { - definition.Kind = c.meshObject.Kind - definition.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Put(ctx, uuid, definition) } diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go index 0c300b4..77bd0ed 100644 --- a/buildingblock_definition_version.go +++ b/buildingblock_definition_version.go @@ -179,11 +179,9 @@ type MeshBuildingBlockDefinitionVersionStatus struct { } type MeshBuildingBlockDefinitionVersion struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshBuildingBlockDefinitionVersionMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshBuildingBlockDefinitionVersionSpec `json:"spec" tfsdk:"spec"` - Status *MeshBuildingBlockDefinitionVersionStatus `json:"status,omitempty" tfsdk:"status"` + Metadata MeshBuildingBlockDefinitionVersionMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockDefinitionVersionSpec `json:"spec" tfsdk:"spec"` + Status *MeshBuildingBlockDefinitionVersionStatus `json:"status,omitempty" tfsdk:"status"` } // MeshBuildingBlockDefinitionVersionClient manages a version of a building block definition. @@ -211,8 +209,6 @@ func (c meshBuildingBlockDefinitionVersionClient) List(ctx context.Context, buil func (c meshBuildingBlockDefinitionVersionClient) Create(ctx context.Context, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) { return c.meshObject.Post(ctx, MeshBuildingBlockDefinitionVersion{ - ApiVersion: c.meshObject.ApiVersion, - Kind: c.meshObject.Kind, Metadata: MeshBuildingBlockDefinitionVersionMetadata{ OwnedByWorkspace: ownedByWorkspace, }, @@ -222,8 +218,6 @@ func (c meshBuildingBlockDefinitionVersionClient) Create(ctx context.Context, ow func (c meshBuildingBlockDefinitionVersionClient) Update(ctx context.Context, uuid, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) { return c.meshObject.Put(ctx, uuid, MeshBuildingBlockDefinitionVersion{ - ApiVersion: c.meshObject.ApiVersion, - Kind: c.meshObject.Kind, Metadata: MeshBuildingBlockDefinitionVersionMetadata{ Uuid: uuid, OwnedByWorkspace: ownedByWorkspace, diff --git a/buildingblock_v2.go b/buildingblock_v2.go index fa3a3d5..db09c73 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -18,11 +18,9 @@ const ( ) type MeshBuildingBlockV2 struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshBuildingBlockV2Metadata `json:"metadata" tfsdk:"metadata"` - Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` - Status MeshBuildingBlockV2Status `json:"status" tfsdk:"status"` + Metadata MeshBuildingBlockV2Metadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` + Status MeshBuildingBlockV2Status `json:"status" tfsdk:"status"` } type MeshBuildingBlockV2Metadata struct { @@ -53,9 +51,7 @@ type MeshBuildingBlockV2TargetRef struct { } type MeshBuildingBlockV2Create struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` + Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` } type MeshBuildingBlockV2Status struct { @@ -64,29 +60,36 @@ type MeshBuildingBlockV2Status struct { ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` } -type MeshBuildingBlockV2Client struct { +type MeshBuildingBlockV2Client interface { + Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) + ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) + Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) + Delete(ctx context.Context, uuid string) error +} + +type meshBuildingBlockV2Client struct { meshObject internal.MeshObjectClient[MeshBuildingBlockV2] } func newBuildingBlockV2Client(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockV2Client { - return MeshBuildingBlockV2Client{internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview")} + return meshBuildingBlockV2Client{internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview")} } -func (c MeshBuildingBlockV2Client) Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { +func (c meshBuildingBlockV2Client) Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { return c.ReadFunc(uuid)(ctx) } -func (c MeshBuildingBlockV2Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) { +func (c meshBuildingBlockV2Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) { return func(ctx context.Context) (*MeshBuildingBlockV2, error) { return c.meshObject.Get(ctx, uuid) } } -func (c MeshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { +func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { return c.meshObject.Post(ctx, bb) } -func (c MeshBuildingBlockV2Client) Delete(ctx context.Context, uuid string) error { +func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string) error { return c.meshObject.Delete(ctx, uuid) } diff --git a/client_kind.go b/client_kind.go new file mode 100644 index 0000000..340a02f --- /dev/null +++ b/client_kind.go @@ -0,0 +1,48 @@ +package client + +// meshObjectKind provides typed constants for meshObject kind strings used across the provider. +type meshObjectKind struct { + BuildingBlock string + BuildingBlockDefinition string + BuildingBlockDefinitionVersion string + BuildingBlockRunner string + Integration string + LandingZone string + Location string + PaymentMethod string + Platform string + PlatformType string + Project string + ProjectGroupBinding string + ProjectRole string + ProjectUserBinding string + ServiceInstance string + TagDefinition string + Tenant string + Workspace string + WorkspaceGroupBinding string + WorkspaceUserBinding string +} + +var MeshObjectKind = meshObjectKind{ + BuildingBlock: "meshBuildingBlock", + BuildingBlockDefinition: "meshBuildingBlockDefinition", + BuildingBlockDefinitionVersion: "meshBuildingBlockDefinitionVersion", + BuildingBlockRunner: "meshBuildingBlockRunner", + Integration: "meshIntegration", + LandingZone: "meshLandingZone", + Location: "meshLocation", + PaymentMethod: "meshPaymentMethod", + Platform: "meshPlatform", + PlatformType: "meshPlatformType", + Project: "meshProject", + ProjectGroupBinding: "meshProjectGroupBinding", + ProjectRole: "meshProjectRole", + ProjectUserBinding: "meshProjectUserBinding", + ServiceInstance: "meshServiceInstance", + TagDefinition: "meshTagDefinition", + Tenant: "meshTenant", + Workspace: "meshWorkspace", + WorkspaceGroupBinding: "meshWorkspaceGroupBinding", + WorkspaceUserBinding: "meshWorkspaceUserBinding", +} diff --git a/client_kind_test.go b/client_kind_test.go new file mode 100644 index 0000000..f011b00 --- /dev/null +++ b/client_kind_test.go @@ -0,0 +1,33 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + +func TestKind(t *testing.T) { + // verify hardcoded kind strings match InferKind for all client types + assert.Equal(t, internal.InferKind[MeshBuildingBlock](), MeshObjectKind.BuildingBlock) + assert.Equal(t, internal.InferKind[MeshBuildingBlockV2](), MeshObjectKind.BuildingBlock) + assert.Equal(t, internal.InferKind[MeshBuildingBlockDefinition](), MeshObjectKind.BuildingBlockDefinition) + assert.Equal(t, internal.InferKind[MeshBuildingBlockDefinitionVersion](), MeshObjectKind.BuildingBlockDefinitionVersion) + assert.Equal(t, internal.InferKind[MeshIntegration](), MeshObjectKind.Integration) + assert.Equal(t, internal.InferKind[MeshLandingZone](), MeshObjectKind.LandingZone) + assert.Equal(t, internal.InferKind[MeshLocation](), MeshObjectKind.Location) + assert.Equal(t, internal.InferKind[MeshPaymentMethod](), MeshObjectKind.PaymentMethod) + assert.Equal(t, internal.InferKind[MeshPlatform](), MeshObjectKind.Platform) + assert.Equal(t, internal.InferKind[MeshPlatformType](), MeshObjectKind.PlatformType) + assert.Equal(t, internal.InferKind[MeshProject](), MeshObjectKind.Project) + assert.Equal(t, internal.InferKind[MeshProjectGroupBinding](), MeshObjectKind.ProjectGroupBinding) + assert.Equal(t, internal.InferKind[MeshProjectUserBinding](), MeshObjectKind.ProjectUserBinding) + assert.Equal(t, internal.InferKind[MeshServiceInstance](), MeshObjectKind.ServiceInstance) + assert.Equal(t, internal.InferKind[MeshTagDefinition](), MeshObjectKind.TagDefinition) + assert.Equal(t, internal.InferKind[MeshTenant](), MeshObjectKind.Tenant) + assert.Equal(t, internal.InferKind[MeshTenantV4](), MeshObjectKind.Tenant) + assert.Equal(t, internal.InferKind[MeshWorkspace](), MeshObjectKind.Workspace) + assert.Equal(t, internal.InferKind[MeshWorkspaceGroupBinding](), MeshObjectKind.WorkspaceGroupBinding) + assert.Equal(t, internal.InferKind[MeshWorkspaceUserBinding](), MeshObjectKind.WorkspaceUserBinding) +} diff --git a/integration.go b/integration.go index 5983c75..78cbdba 100644 --- a/integration.go +++ b/integration.go @@ -7,11 +7,9 @@ import ( ) type MeshIntegration struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Kind string `json:"kind" tfsdk:"-"` - Metadata MeshIntegrationMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshIntegrationSpec `json:"spec" tfsdk:"spec"` - Status *MeshIntegrationStatus `json:"status" tfsdk:"status"` + Metadata MeshIntegrationMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshIntegrationSpec `json:"spec" tfsdk:"spec"` + Status *MeshIntegrationStatus `json:"status" tfsdk:"status"` } type MeshIntegrationMetadata struct { @@ -63,8 +61,6 @@ func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) } func (c meshIntegrationClientImpl) Create(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) { - integration.Kind = c.meshObject.Kind - integration.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Post(ctx, integration) } @@ -73,8 +69,6 @@ func (c meshIntegrationClientImpl) Read(ctx context.Context, uuid string) (*Mesh } func (c meshIntegrationClientImpl) Update(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) { - integration.Kind = c.meshObject.Kind - integration.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Put(ctx, *integration.Metadata.Uuid, integration) } diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 7957654..dcd65b8 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -2,6 +2,7 @@ package internal import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -31,31 +32,31 @@ type MeshObjectClient[M any] struct { // The API URL is constructed from explicitApiPathElems if provided, // otherwise the pluralized and lowercased kind is used as a single element. func NewMeshObjectClient[M any](ctx context.Context, httpClient *HttpClient, apiVersion string, explicitApiPathElems ...string) MeshObjectClient[M] { - kind, typeName := inferMeshObjectKindFromType[M]() + kind := InferKind[M]() if len(explicitApiPathElems) == 0 { explicitApiPathElems = []string{strings.ToLower(pluralizeKind(kind))} } explicitApiPathElems = slices.Insert(explicitApiPathElems, 0, "/api/meshobjects") apiUrl := httpClient.RootUrl.JoinPath(explicitApiPathElems...) - Log.Info(ctx, fmt.Sprintf("initialized %s client", typeName), "url", apiUrl.String(), "kind", kind, "version", apiVersion) + Log.Info(ctx, fmt.Sprintf("initialized %s client", reflect.TypeFor[M]().Name()), "url", apiUrl.String(), "kind", kind, "version", apiVersion) return MeshObjectClient[M]{httpClient, kind, apiVersion, apiUrl} } -func inferMeshObjectKindFromType[M any]() (lowercase, typeName string) { - var zero M - typeName = reflect.TypeOf(zero).Name() - lowercase = lowercaseFirst(typeName) - return regexp.MustCompile(`V\d+$`).ReplaceAllString(lowercase, ""), typeName -} +var versionSuffixRe = regexp.MustCompile(`V\d+$`) -func lowercaseFirst(s string) string { - if s == "" { - return s - } - runes := []rune(s) +// InferKind infers the meshObject kind from a struct type name using the same convention +// as the meshObject API: MeshWorkspace → "meshWorkspace", MeshTenantV4 → "meshTenant". +// Version suffixes (V\d+) are stripped. +// Tested when client.Kind is statically initialized. +func InferKind[M any]() string { + typeName := reflect.TypeFor[M]().Name() + + runes := []rune(typeName) runes[0] = unicode.ToLower(runes[0]) - return string(runes) + kind := string(runes) + + return versionSuffixRe.ReplaceAllString(kind, "") } func pluralizeKind(kind string) string { @@ -80,13 +81,38 @@ func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (*M, error) { } // Post creates a new meshObject with the given payload. +// Automatically injects apiVersion and kind into the JSON payload. func (c MeshObjectClient[M]) Post(ctx context.Context, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPost, c.ApiUrl, withPayload(payload, c.meshObjectMimeType()))) + return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPost, c.ApiUrl, c.withMeshObjectPayload(payload))) } // Put updates an existing meshObject by ID with the given payload. +// Automatically injects apiVersion and kind into the JSON payload. func (c MeshObjectClient[M]) Put(ctx context.Context, id string, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPut, c.ApiUrl.JoinPath(id), withPayload(payload, c.meshObjectMimeType()))) + return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPut, c.ApiUrl.JoinPath(id), c.withMeshObjectPayload(payload))) +} + +// withMeshObjectPayload returns a RequestOption that sets the payload with apiVersion and kind injected, +// using the meshObject MIME type for content negotiation. +// Panics on marshal errors which indicates a programming error (payload is always a well-typed struct). +// +// The double marshal/unmarshal round-trip converts the typed struct to a map[string]any so we can +// inject the top-level apiVersion and kind fields without coupling the struct type to those fields. +func (c MeshObjectClient[M]) withMeshObjectPayload(payload any) RequestOption { + intermediate, err := json.Marshal(payload) + if err != nil { + panic(fmt.Sprintf("failed to marshal %T: %v", payload, err)) + } + + var m map[string]any + if err := json.Unmarshal(intermediate, &m); err != nil { + panic(fmt.Sprintf("failed to unmarshal %T to map: %v", payload, err)) + } + + m["apiVersion"] = c.ApiVersion + m["kind"] = c.Kind + + return withPayload(m, c.meshObjectMimeType()) } // Delete removes a meshObject by ID. diff --git a/internal/mesh_object_client_test.go b/internal/mesh_object_client_test.go deleted file mode 100644 index fb8f068..0000000 --- a/internal/mesh_object_client_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package internal - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -type MeshBuildingBlock struct{} -type MeshBuildingBlockV2 struct{} -type MeshTenantV4 struct{} -type MeshWorkspace struct{} - -func Test_inferMeshObjectKindFromType(t *testing.T) { - tests := []struct { - kind string - testFunc func() (string, string) - expected string - }{ - { - kind: "MeshBuildingBlock", - testFunc: inferMeshObjectKindFromType[MeshBuildingBlock], - expected: "meshBuildingBlock", - }, - { - kind: "MeshBuildingBlockV2", - testFunc: inferMeshObjectKindFromType[MeshBuildingBlockV2], - expected: "meshBuildingBlock", - }, - { - kind: "MeshWorkspace", - testFunc: inferMeshObjectKindFromType[MeshWorkspace], - expected: "meshWorkspace", - }, - { - kind: "MeshTenantV4", - testFunc: inferMeshObjectKindFromType[MeshTenantV4], - expected: "meshTenant", - }, - } - - for _, tt := range tests { - t.Run(tt.kind, func(t *testing.T) { - actual, _ := tt.testFunc() - assert.Equal(t, tt.expected, actual) - }) - } -} diff --git a/landingzone.go b/landingzone.go index 1e8e33e..157d164 100644 --- a/landingzone.go +++ b/landingzone.go @@ -7,11 +7,9 @@ import ( ) type MeshLandingZone struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Kind string `json:"kind" tfsdk:"-"` - Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` - Status MeshLandingZoneStatus `json:"status" tfsdk:"status"` + Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` + Status MeshLandingZoneStatus `json:"status" tfsdk:"status"` } type MeshLandingZoneMetadata struct { @@ -61,31 +59,37 @@ type MeshLandingZoneQuota struct { } type MeshLandingZoneCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` + Metadata MeshLandingZoneMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` } -type MeshLandingZoneClient struct { +type MeshLandingZoneClient interface { + Read(ctx context.Context, name string) (*MeshLandingZone, error) + Create(ctx context.Context, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) + Update(ctx context.Context, name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) + Delete(ctx context.Context, name string) error +} + +type meshLandingZoneClient struct { meshObject internal.MeshObjectClient[MeshLandingZone] } func newLandingZoneClient(ctx context.Context, httpClient *internal.HttpClient) MeshLandingZoneClient { - return MeshLandingZoneClient{internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1")} + return meshLandingZoneClient{internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1")} } -func (c MeshLandingZoneClient) Read(ctx context.Context, name string) (*MeshLandingZone, error) { +func (c meshLandingZoneClient) Read(ctx context.Context, name string) (*MeshLandingZone, error) { return c.meshObject.Get(ctx, name) } -func (c MeshLandingZoneClient) Create(ctx context.Context, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { +func (c meshLandingZoneClient) Create(ctx context.Context, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { return c.meshObject.Post(ctx, landingZone) } -func (c MeshLandingZoneClient) Update(ctx context.Context, name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { +func (c meshLandingZoneClient) Update(ctx context.Context, name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { return c.meshObject.Put(ctx, name, landingZone) } -func (c MeshLandingZoneClient) Delete(ctx context.Context, name string) error { +func (c meshLandingZoneClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } diff --git a/location.go b/location.go index 9a25b33..33bf0a8 100644 --- a/location.go +++ b/location.go @@ -7,10 +7,9 @@ import ( ) type MeshLocation struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Metadata MeshLocationMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` - Status MeshLocationStatus `json:"status" tfsdk:"status"` + Metadata MeshLocationMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` + Status MeshLocationStatus `json:"status" tfsdk:"status"` } type MeshLocationMetadata struct { @@ -29,9 +28,8 @@ type MeshLocationStatus struct { } type MeshLocationCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Metadata MeshLocationCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` + Metadata MeshLocationCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshLocationSpec `json:"spec" tfsdk:"spec"` } type MeshLocationCreateMetadata struct { diff --git a/payment_method.go b/payment_method.go index 98111ff..6867053 100644 --- a/payment_method.go +++ b/payment_method.go @@ -7,10 +7,8 @@ import ( ) type MeshPaymentMethod struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshPaymentMethodMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPaymentMethodSpec `json:"spec" tfsdk:"spec"` + Metadata MeshPaymentMethodMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPaymentMethodSpec `json:"spec" tfsdk:"spec"` } type MeshPaymentMethodMetadata struct { @@ -28,9 +26,8 @@ type MeshPaymentMethodSpec struct { } type MeshPaymentMethodCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Metadata MeshPaymentMethodCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPaymentMethodSpec `json:"spec" tfsdk:"spec"` + Metadata MeshPaymentMethodCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPaymentMethodSpec `json:"spec" tfsdk:"spec"` } type MeshPaymentMethodCreateMetadata struct { @@ -38,26 +35,33 @@ type MeshPaymentMethodCreateMetadata struct { OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -type MeshPaymentMethodClient struct { +type MeshPaymentMethodClient interface { + Read(ctx context.Context, workspace string, identifier string) (*MeshPaymentMethod, error) + Create(ctx context.Context, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) + Update(ctx context.Context, identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) + Delete(ctx context.Context, identifier string) error +} + +type meshPaymentMethodClient struct { meshObject internal.MeshObjectClient[MeshPaymentMethod] } func newPaymentMethodClient(ctx context.Context, httpClient *internal.HttpClient) MeshPaymentMethodClient { - return MeshPaymentMethodClient{internal.NewMeshObjectClient[MeshPaymentMethod](ctx, httpClient, "v2")} + return meshPaymentMethodClient{internal.NewMeshObjectClient[MeshPaymentMethod](ctx, httpClient, "v2")} } -func (c MeshPaymentMethodClient) Read(ctx context.Context, workspace string, identifier string) (*MeshPaymentMethod, error) { +func (c meshPaymentMethodClient) Read(ctx context.Context, workspace string, identifier string) (*MeshPaymentMethod, error) { return c.meshObject.Get(ctx, identifier) } -func (c MeshPaymentMethodClient) Create(ctx context.Context, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { +func (c meshPaymentMethodClient) Create(ctx context.Context, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { return c.meshObject.Post(ctx, paymentMethod) } -func (c MeshPaymentMethodClient) Update(ctx context.Context, identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { +func (c meshPaymentMethodClient) Update(ctx context.Context, identifier string, paymentMethod *MeshPaymentMethodCreate) (*MeshPaymentMethod, error) { return c.meshObject.Put(ctx, identifier, paymentMethod) } -func (c MeshPaymentMethodClient) Delete(ctx context.Context, identifier string) error { +func (c meshPaymentMethodClient) Delete(ctx context.Context, identifier string) error { return c.meshObject.Delete(ctx, identifier) } diff --git a/platform.go b/platform.go index cc7133c..c276a8d 100644 --- a/platform.go +++ b/platform.go @@ -8,10 +8,8 @@ import ( ) type MeshPlatform struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Kind string `json:"kind" tfsdk:"-"` - Metadata MeshPlatformMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` + Metadata MeshPlatformMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformSpec `json:"spec" tfsdk:"spec"` } type MeshPlatformMetadata struct { @@ -101,14 +99,10 @@ func (c meshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatfor } func (c meshPlatformClient) Create(ctx context.Context, platform MeshPlatform) (*MeshPlatform, error) { - platform.Kind = c.meshObject.Kind - platform.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Post(ctx, platform) } func (c meshPlatformClient) Update(ctx context.Context, uuid string, platform MeshPlatform) (*MeshPlatform, error) { - platform.Kind = c.meshObject.Kind - platform.ApiVersion = c.meshObject.ApiVersion return c.meshObject.Put(ctx, uuid, platform) } diff --git a/platform_type.go b/platform_type.go index 29df7a2..6160b32 100644 --- a/platform_type.go +++ b/platform_type.go @@ -7,11 +7,9 @@ import ( ) type MeshPlatformType struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Kind string `json:"kind" tfsdk:"-"` - Metadata MeshPlatformTypeMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` - Status MeshPlatformTypeStatus `json:"status" tfsdk:"status"` + Metadata MeshPlatformTypeMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` + Status MeshPlatformTypeStatus `json:"status" tfsdk:"status"` } type MeshPlatformTypeStatus struct { @@ -36,10 +34,8 @@ type MeshPlatformTypeSpec struct { } type MeshPlatformTypeCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"-"` - Kind string `json:"kind" tfsdk:"-"` - Metadata MeshPlatformTypeCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` + Metadata MeshPlatformTypeCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshPlatformTypeSpec `json:"spec" tfsdk:"spec"` } type MeshPlatformTypeCreateMetadata struct { diff --git a/project.go b/project.go index d8a2fba..51e7477 100644 --- a/project.go +++ b/project.go @@ -7,10 +7,8 @@ import ( ) type MeshProject struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshProjectMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshProjectSpec `json:"spec" tfsdk:"spec"` + Metadata MeshProjectMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshProjectSpec `json:"spec" tfsdk:"spec"` } type MeshProjectMetadata struct { @@ -37,23 +35,31 @@ type MeshProjectCreateMetadata struct { OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } -type MeshProjectClient struct { +type MeshProjectClient interface { + Read(ctx context.Context, workspace string, name string) (*MeshProject, error) + List(ctx context.Context, workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) + Create(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) + Update(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) + Delete(ctx context.Context, workspace string, name string) error +} + +type meshProjectClient struct { meshObject internal.MeshObjectClient[MeshProject] } func newProjectClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectClient { - return MeshProjectClient{internal.NewMeshObjectClient[MeshProject](ctx, httpClient, "v2")} + return meshProjectClient{internal.NewMeshObjectClient[MeshProject](ctx, httpClient, "v2")} } -func (c MeshProjectClient) projectId(workspace string, name string) string { +func (c meshProjectClient) projectId(workspace string, name string) string { return workspace + "." + name } -func (c MeshProjectClient) Read(ctx context.Context, workspace string, name string) (*MeshProject, error) { +func (c meshProjectClient) Read(ctx context.Context, workspace string, name string) (*MeshProject, error) { return c.meshObject.Get(ctx, c.projectId(workspace, name)) } -func (c MeshProjectClient) List(ctx context.Context, workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { +func (c meshProjectClient) List(ctx context.Context, workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { options := []internal.RequestOption{ internal.WithUrlQuery("workspaceIdentifier", workspaceIdentifier), } @@ -63,14 +69,14 @@ func (c MeshProjectClient) List(ctx context.Context, workspaceIdentifier string, return c.meshObject.List(ctx, options...) } -func (c MeshProjectClient) Create(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { +func (c meshProjectClient) Create(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { return c.meshObject.Post(ctx, project) } -func (c MeshProjectClient) Update(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { +func (c meshProjectClient) Update(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { return c.meshObject.Put(ctx, c.projectId(project.Metadata.OwnedByWorkspace, project.Metadata.Name), project) } -func (c MeshProjectClient) Delete(ctx context.Context, workspace string, name string) error { +func (c meshProjectClient) Delete(ctx context.Context, workspace string, name string) error { return c.meshObject.Delete(ctx, c.projectId(workspace, name)) } diff --git a/project_binding.go b/project_binding.go index 96e8b69..4178b68 100644 --- a/project_binding.go +++ b/project_binding.go @@ -1,12 +1,10 @@ package client type MeshProjectBinding struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshProjectBindingMetadata `json:"metadata" tfsdk:"metadata"` - RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` - TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshSubject `json:"subject" tfsdk:"subject"` + Metadata MeshProjectBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshProjectRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshProjectTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshSubject `json:"subject" tfsdk:"subject"` } type MeshProjectBindingMetadata struct { diff --git a/project_group_binding.go b/project_group_binding.go index 916d0e6..a19f970 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -10,22 +10,28 @@ type MeshProjectGroupBinding struct { MeshProjectBinding } -type MeshProjectGroupBindingClient struct { +type MeshProjectGroupBindingClient interface { + Read(ctx context.Context, name string) (*MeshProjectGroupBinding, error) + Create(ctx context.Context, binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) + Delete(ctx context.Context, name string) error +} + +type meshProjectGroupBindingClient struct { meshObject internal.MeshObjectClient[MeshProjectGroupBinding] } func newProjectGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectGroupBindingClient { - return MeshProjectGroupBindingClient{internal.NewMeshObjectClient[MeshProjectGroupBinding](ctx, httpClient, "v3", "meshprojectbindings", "groupbindings")} + return meshProjectGroupBindingClient{internal.NewMeshObjectClient[MeshProjectGroupBinding](ctx, httpClient, "v3", "meshprojectbindings", "groupbindings")} } -func (c MeshProjectGroupBindingClient) Read(ctx context.Context, name string) (*MeshProjectGroupBinding, error) { +func (c meshProjectGroupBindingClient) Read(ctx context.Context, name string) (*MeshProjectGroupBinding, error) { return c.meshObject.Get(ctx, name) } -func (c MeshProjectGroupBindingClient) Create(ctx context.Context, binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { +func (c meshProjectGroupBindingClient) Create(ctx context.Context, binding *MeshProjectGroupBinding) (*MeshProjectGroupBinding, error) { return c.meshObject.Post(ctx, binding) } -func (c MeshProjectGroupBindingClient) Delete(ctx context.Context, name string) error { +func (c meshProjectGroupBindingClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } diff --git a/project_user_binding.go b/project_user_binding.go index 2ddcd6b..75c828f 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -10,22 +10,28 @@ type MeshProjectUserBinding struct { MeshProjectBinding } -type MeshProjectUserBindingClient struct { +type MeshProjectUserBindingClient interface { + Read(ctx context.Context, name string) (*MeshProjectUserBinding, error) + Create(ctx context.Context, binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) + Delete(ctx context.Context, name string) error +} + +type meshProjectUserBindingClient struct { meshObject internal.MeshObjectClient[MeshProjectUserBinding] } func newProjectUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectUserBindingClient { - return MeshProjectUserBindingClient{internal.NewMeshObjectClient[MeshProjectUserBinding](ctx, httpClient, "v3", "meshprojectbindings", "userbindings")} + return meshProjectUserBindingClient{internal.NewMeshObjectClient[MeshProjectUserBinding](ctx, httpClient, "v3", "meshprojectbindings", "userbindings")} } -func (c MeshProjectUserBindingClient) Read(ctx context.Context, name string) (*MeshProjectUserBinding, error) { +func (c meshProjectUserBindingClient) Read(ctx context.Context, name string) (*MeshProjectUserBinding, error) { return c.meshObject.Get(ctx, name) } -func (c MeshProjectUserBindingClient) Create(ctx context.Context, binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { +func (c meshProjectUserBindingClient) Create(ctx context.Context, binding *MeshProjectUserBinding) (*MeshProjectUserBinding, error) { return c.meshObject.Post(ctx, binding) } -func (c MeshProjectUserBindingClient) Delete(ctx context.Context, name string) error { +func (c meshProjectUserBindingClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } diff --git a/service_instance.go b/service_instance.go index 3312b61..2cd537b 100644 --- a/service_instance.go +++ b/service_instance.go @@ -8,10 +8,8 @@ import ( ) type MeshServiceInstance struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshServiceInstanceMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshServiceInstanceSpec `json:"spec" tfsdk:"spec"` + Metadata MeshServiceInstanceMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshServiceInstanceSpec `json:"spec" tfsdk:"spec"` } type MeshServiceInstanceMetadata struct { diff --git a/tag_definition.go b/tag_definition.go index dfc6300..61b11bd 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -9,10 +9,8 @@ import ( const API_VERSION_TAG_DEFINITION = "v1" 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"` + Metadata MeshTagDefinitionMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTagDefinitionSpec `json:"spec" tfsdk:"spec"` } type MeshTagDefinitionMetadata struct { diff --git a/tenant.go b/tenant.go index c68e9fb..d7929d3 100644 --- a/tenant.go +++ b/tenant.go @@ -7,10 +7,8 @@ import ( ) type MeshTenant struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshTenantMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantSpec `json:"spec" tfsdk:"spec"` + Metadata MeshTenantMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantSpec `json:"spec" tfsdk:"spec"` } type MeshTenantMetadata struct { @@ -49,26 +47,32 @@ type MeshTenantCreateSpec struct { Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } -type MeshTenantClient struct { +type MeshTenantClient interface { + Read(ctx context.Context, workspace string, project string, platform string) (*MeshTenant, error) + Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) + Delete(ctx context.Context, workspace string, project string, platform string) error +} + +type meshTenantClient struct { meshObject internal.MeshObjectClient[MeshTenant] } func newTenantClient(ctx context.Context, httpClient *internal.HttpClient) MeshTenantClient { - return MeshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3")} + return meshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3")} } -func (c MeshTenantClient) tenantId(workspace string, project string, platform string) string { +func (c meshTenantClient) tenantId(workspace string, project string, platform string) string { return workspace + "." + project + "." + platform } -func (c MeshTenantClient) Read(ctx context.Context, workspace string, project string, platform string) (*MeshTenant, error) { +func (c meshTenantClient) Read(ctx context.Context, workspace string, project string, platform string) (*MeshTenant, error) { return c.meshObject.Get(ctx, c.tenantId(workspace, project, platform)) } -func (c MeshTenantClient) Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) { +func (c meshTenantClient) Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) { return c.meshObject.Post(ctx, tenant) } -func (c MeshTenantClient) Delete(ctx context.Context, workspace string, project string, platform string) error { +func (c meshTenantClient) Delete(ctx context.Context, workspace string, project string, platform string) error { return c.meshObject.Delete(ctx, c.tenantId(workspace, project, platform)) } diff --git a/tenant_v4.go b/tenant_v4.go index 4d6f317..5e1bfc8 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -8,11 +8,9 @@ import ( ) type MeshTenantV4 struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshTenantV4Metadata `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantV4Spec `json:"spec" tfsdk:"spec"` - Status MeshTenantV4Status `json:"status" tfsdk:"status"` + Metadata MeshTenantV4Metadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantV4Spec `json:"spec" tfsdk:"spec"` + Status MeshTenantV4Status `json:"status" tfsdk:"status"` } type MeshTenantV4Metadata struct { @@ -55,29 +53,36 @@ type MeshTenantV4CreateSpec struct { Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } -type MeshTenantV4Client struct { +type MeshTenantV4Client interface { + Read(ctx context.Context, uuid string) (*MeshTenantV4, error) + ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) + Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) + Delete(ctx context.Context, uuid string) error +} + +type meshTenantV4Client struct { meshObject internal.MeshObjectClient[MeshTenantV4] } func newTenantV4Client(ctx context.Context, httpClient *internal.HttpClient) MeshTenantV4Client { - return MeshTenantV4Client{internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview")} + return meshTenantV4Client{internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview")} } -func (c MeshTenantV4Client) Read(ctx context.Context, uuid string) (*MeshTenantV4, error) { +func (c meshTenantV4Client) Read(ctx context.Context, uuid string) (*MeshTenantV4, error) { return c.ReadFunc(uuid)(ctx) } -func (c MeshTenantV4Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) { +func (c meshTenantV4Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) { return func(ctx context.Context) (*MeshTenantV4, error) { return c.meshObject.Get(ctx, uuid) } } -func (c MeshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) { +func (c meshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) { return c.meshObject.Post(ctx, tenant) } -func (c MeshTenantV4Client) Delete(ctx context.Context, uuid string) error { +func (c meshTenantV4Client) Delete(ctx context.Context, uuid string) error { return c.meshObject.Delete(ctx, uuid) } diff --git a/workspace.go b/workspace.go index e019308..1f47fa6 100644 --- a/workspace.go +++ b/workspace.go @@ -7,10 +7,8 @@ import ( ) type MeshWorkspace struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshWorkspaceMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` + Metadata MeshWorkspaceMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` } type MeshWorkspaceMetadata struct { @@ -26,35 +24,41 @@ type MeshWorkspaceSpec struct { } type MeshWorkspaceCreate struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Metadata MeshWorkspaceCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` + Metadata MeshWorkspaceCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` } type MeshWorkspaceCreateMetadata struct { Name string `json:"name" tfsdk:"name"` Tags map[string][]string `json:"tags" tfsdk:"tags"` } -type MeshWorkspaceClient struct { +type MeshWorkspaceClient interface { + Read(ctx context.Context, name string) (*MeshWorkspace, error) + Create(ctx context.Context, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) + Update(ctx context.Context, name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) + Delete(ctx context.Context, name string) error +} + +type meshWorkspaceClient struct { meshObject internal.MeshObjectClient[MeshWorkspace] } -func newWorkspaceClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceClient { - return MeshWorkspaceClient{internal.NewMeshObjectClient[MeshWorkspace](ctx, httpClient, "v2")} +func newWorkspaceClient(ctx context.Context, httpClient *internal.HttpClient) meshWorkspaceClient { + return meshWorkspaceClient{internal.NewMeshObjectClient[MeshWorkspace](ctx, httpClient, "v2")} } -func (c MeshWorkspaceClient) Read(ctx context.Context, name string) (*MeshWorkspace, error) { +func (c meshWorkspaceClient) Read(ctx context.Context, name string) (*MeshWorkspace, error) { return c.meshObject.Get(ctx, name) } -func (c MeshWorkspaceClient) Create(ctx context.Context, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { +func (c meshWorkspaceClient) Create(ctx context.Context, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { return c.meshObject.Post(ctx, workspace) } -func (c MeshWorkspaceClient) Update(ctx context.Context, name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { +func (c meshWorkspaceClient) Update(ctx context.Context, name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { return c.meshObject.Put(ctx, name, workspace) } -func (c MeshWorkspaceClient) Delete(ctx context.Context, name string) error { +func (c meshWorkspaceClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } diff --git a/workspace_binding.go b/workspace_binding.go index 8732b7c..fc6a253 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -1,12 +1,10 @@ package client type MeshWorkspaceBinding struct { - ApiVersion string `json:"apiVersion" tfsdk:"api_version"` - Kind string `json:"kind" tfsdk:"kind"` - Metadata MeshWorkspaceBindingMetadata `json:"metadata" tfsdk:"metadata"` - RoleRef MeshWorkspaceRoleRef `json:"roleRef" tfsdk:"role_ref"` - TargetRef MeshWorkspaceTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` + Metadata MeshWorkspaceBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshWorkspaceRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshWorkspaceTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` } type MeshWorkspaceBindingMetadata struct { diff --git a/workspace_group_binding.go b/workspace_group_binding.go index e4404a6..027afb0 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -10,22 +10,28 @@ type MeshWorkspaceGroupBinding struct { MeshWorkspaceBinding } -type MeshWorkspaceGroupBindingClient struct { +type MeshWorkspaceGroupBindingClient interface { + Read(ctx context.Context, name string) (*MeshWorkspaceGroupBinding, error) + Create(ctx context.Context, binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) + Delete(ctx context.Context, name string) error +} + +type meshWorkspaceGroupBindingClient struct { meshObject internal.MeshObjectClient[MeshWorkspaceGroupBinding] } func newWorkspaceGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceGroupBindingClient { - return MeshWorkspaceGroupBindingClient{internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](ctx, httpClient, "v2", "meshworkspacebindings", "groupbindings")} + return meshWorkspaceGroupBindingClient{internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](ctx, httpClient, "v2", "meshworkspacebindings", "groupbindings")} } -func (c MeshWorkspaceGroupBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceGroupBinding, error) { +func (c meshWorkspaceGroupBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceGroupBinding, error) { return c.meshObject.Get(ctx, name) } -func (c MeshWorkspaceGroupBindingClient) Create(ctx context.Context, binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { +func (c meshWorkspaceGroupBindingClient) Create(ctx context.Context, binding *MeshWorkspaceGroupBinding) (*MeshWorkspaceGroupBinding, error) { return c.meshObject.Post(ctx, binding) } -func (c MeshWorkspaceGroupBindingClient) Delete(ctx context.Context, name string) error { +func (c meshWorkspaceGroupBindingClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index eb552d1..501f4ee 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -10,22 +10,28 @@ type MeshWorkspaceUserBinding struct { MeshWorkspaceBinding } -type MeshWorkspaceUserBindingClient struct { +type MeshWorkspaceUserBindingClient interface { + Read(ctx context.Context, name string) (*MeshWorkspaceUserBinding, error) + Create(ctx context.Context, binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) + Delete(ctx context.Context, name string) error +} + +type meshWorkspaceUserBindingClient struct { meshObject internal.MeshObjectClient[MeshWorkspaceUserBinding] } func newWorkspaceUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceUserBindingClient { - return MeshWorkspaceUserBindingClient{internal.NewMeshObjectClient[MeshWorkspaceUserBinding](ctx, httpClient, "v2", "meshworkspacebindings", "userbindings")} + return meshWorkspaceUserBindingClient{internal.NewMeshObjectClient[MeshWorkspaceUserBinding](ctx, httpClient, "v2", "meshworkspacebindings", "userbindings")} } -func (c MeshWorkspaceUserBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceUserBinding, error) { +func (c meshWorkspaceUserBindingClient) Read(ctx context.Context, name string) (*MeshWorkspaceUserBinding, error) { return c.meshObject.Get(ctx, name) } -func (c MeshWorkspaceUserBindingClient) Create(ctx context.Context, binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { +func (c meshWorkspaceUserBindingClient) Create(ctx context.Context, binding *MeshWorkspaceUserBinding) (*MeshWorkspaceUserBinding, error) { return c.meshObject.Post(ctx, binding) } -func (c MeshWorkspaceUserBindingClient) Delete(ctx context.Context, name string) error { +func (c meshWorkspaceUserBindingClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } From 2094fae685af3aa09a9cbfd873044cdbfdab60b7 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 9 Apr 2026 16:28:43 +0200 Subject: [PATCH 125/200] feat: add meshstack_tenants data source FEATURES: - New meshstack_tenants data source for listing tenants with optional workspace/project/platform filters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tenant_v4.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tenant_v4.go b/tenant_v4.go index 5e1bfc8..8625136 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -53,9 +53,19 @@ type MeshTenantV4CreateSpec struct { Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } +type MeshTenantV4Query struct { + Workspace string + Project *string + Platform *string + PlatformType *string + LandingZone *string + PlatformTenant *string +} + type MeshTenantV4Client interface { Read(ctx context.Context, uuid string) (*MeshTenantV4, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) + List(ctx context.Context, query *MeshTenantV4Query) ([]MeshTenantV4, error) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) Delete(ctx context.Context, uuid string) error } @@ -82,6 +92,28 @@ func (c meshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Crea return c.meshObject.Post(ctx, tenant) } +func (c meshTenantV4Client) List(ctx context.Context, query *MeshTenantV4Query) ([]MeshTenantV4, error) { + options := []internal.RequestOption{ + internal.WithUrlQuery("workspaceIdentifier", query.Workspace), + } + if query.Project != nil { + options = append(options, internal.WithUrlQuery("projectIdentifier", *query.Project)) + } + if query.Platform != nil { + options = append(options, internal.WithUrlQuery("platformIdentifier", *query.Platform)) + } + if query.PlatformType != nil { + options = append(options, internal.WithUrlQuery("platformTypeIdentifier", *query.PlatformType)) + } + if query.LandingZone != nil { + options = append(options, internal.WithUrlQuery("landingZoneIdentifier", *query.LandingZone)) + } + if query.PlatformTenant != nil { + options = append(options, internal.WithUrlQuery("platformTenantId", *query.PlatformTenant)) + } + return c.meshObject.List(ctx, options...) +} + func (c meshTenantV4Client) Delete(ctx context.Context, uuid string) error { return c.meshObject.Delete(ctx, uuid) } From 030131479e74369626ae1ae12448e814c20d8ceb Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 14 Apr 2026 07:56:08 +0200 Subject: [PATCH 126/200] feat: expose platform access_information in resource and data source Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- platform.go | 1 + 1 file changed, 1 insertion(+) diff --git a/platform.go b/platform.go index c276a8d..08623ea 100644 --- a/platform.go +++ b/platform.go @@ -24,6 +24,7 @@ type MeshPlatformSpec struct { Endpoint string `json:"endpoint" tfsdk:"endpoint"` SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + AccessInformation *string `json:"accessInformation,omitempty" tfsdk:"access_information"` LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` ContributingWorkspaces types.Set[string] `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` Availability PlatformAvailability `json:"availability" tfsdk:"availability"` From 88732b604446ac9ce1b7538d750417c7d67f5c11 Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Thu, 2 Apr 2026 08:59:51 +0200 Subject: [PATCH 127/200] feat: add refName property to AzureDevOps Building Block Definition implementation --- buildingblock_definition_version_implementation.go | 1 + 1 file changed, 1 insertion(+) diff --git a/buildingblock_definition_version_implementation.go b/buildingblock_definition_version_implementation.go index 9b67e9f..c16c397 100644 --- a/buildingblock_definition_version_implementation.go +++ b/buildingblock_definition_version_implementation.go @@ -61,6 +61,7 @@ type MeshBuildingBlockDefinitionGitLabPipelineImplementation struct { type MeshBuildingBlockDefinitionAzureDevOpsPipelineImplementation struct { Project string `json:"project" tfsdk:"project"` PipelineID string `json:"pipelineId" tfsdk:"pipeline_id"` + RefName *string `json:"refName,omitempty" tfsdk:"ref_name"` Async bool `json:"async" tfsdk:"async"` IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` } From 536cadd28098da521ac7da8879adecfbf647e2b5 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 23 Apr 2026 10:42:57 +0200 Subject: [PATCH 128/200] refactor: use Go 1.26 new(expression) feature --- buildingblock_definition_version_test.go | 3 +-- types/clienttypes_test.go | 4 +--- types/enum/enum.go | 4 +--- types/ptr/pointer.go | 5 ----- 4 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 types/ptr/pointer.go diff --git a/buildingblock_definition_version_test.go b/buildingblock_definition_version_test.go index 884a3ce..302b8c1 100644 --- a/buildingblock_definition_version_test.go +++ b/buildingblock_definition_version_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/meshcloud/terraform-provider-meshstack/client/types" - "github.com/meshcloud/terraform-provider-meshstack/client/types/ptr" ) var ( @@ -29,7 +28,7 @@ func TestMeshBuildingBlockDefinitionInput_UnmarshalJSON(t *testing.T) { {"empty", false, types.SecretOrAny{}, types.SecretOrAny{}, assert.NoError}, {"not_sensitive", false, types.SecretOrAny{Y: true}, types.SecretOrAny{Y: "some-string"}, assert.NoError}, {"not_sensitive_but_hash", false, types.SecretOrAny{Y: map[string]any{"hash": "some-hash-looks-like-secret"}}, types.SecretOrAny{}, assert.NoError}, - {"sensitive", true, types.SecretOrAny{}, types.SecretOrAny{X: types.Secret{Hash: ptr.To("some-hash")}}, assert.NoError}, + {"sensitive", true, types.SecretOrAny{}, types.SecretOrAny{X: types.Secret{Hash: new("some-hash")}}, assert.NoError}, {"sensitive_but_no_hash", true, types.SecretOrAny{Y: map[string]any{}}, types.SecretOrAny{}, func(t assert.TestingT, err error, msgAndArgs ...any) bool { return assert.ErrorContains(t, err, "got sensitive argument or default_value but variant Y is set instead") }}, diff --git a/types/clienttypes_test.go b/types/clienttypes_test.go index 8c4e7f3..569bf72 100644 --- a/types/clienttypes_test.go +++ b/types/clienttypes_test.go @@ -7,8 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/meshcloud/terraform-provider-meshstack/client/types/ptr" ) func TestSecretOrAny(t *testing.T) { @@ -21,7 +19,7 @@ func TestSecretOrAny(t *testing.T) { } tests := []testCase{ {"empty", `null`, SecretOrAny{}, false, false}, - {"X plaintext", `{"plaintext":"some-secret"}`, SecretOrAny{X: Secret{Plaintext: ptr.To("some-secret")}}, true, false}, + {"X plaintext", `{"plaintext":"some-secret"}`, SecretOrAny{X: Secret{Plaintext: new("some-secret")}}, true, false}, {"Y string", `"some-string"`, SecretOrAny{Y: "some-string"}, false, true}, {"Y bool", `true`, SecretOrAny{Y: true}, false, true}, {"Y number", `1.23123`, SecretOrAny{Y: 1.23123}, false, true}, diff --git a/types/enum/enum.go b/types/enum/enum.go index 4934aa7..0f07fef 100644 --- a/types/enum/enum.go +++ b/types/enum/enum.go @@ -3,8 +3,6 @@ package enum import ( "fmt" "strings" - - "github.com/meshcloud/terraform-provider-meshstack/client/types/ptr" ) func Of[T ~string](entries ...Entry[T]) Enum[T] { @@ -37,7 +35,7 @@ func (e Enum[T]) Markdown() string { type Entry[T ~string] string func (ee Entry[T]) Ptr() *T { - return ptr.To(ee.Unwrap()) + return new(ee.Unwrap()) } func (ee Entry[T]) Unwrap() T { diff --git a/types/ptr/pointer.go b/types/ptr/pointer.go deleted file mode 100644 index 6c3ee9b..0000000 --- a/types/ptr/pointer.go +++ /dev/null @@ -1,5 +0,0 @@ -package ptr - -func To[T any](v T) *T { - return &v -} From dbece1a8fbd316c40fce3746d941d959bc9aa63b Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Mon, 4 May 2026 17:42:11 +0200 Subject: [PATCH 129/200] feat: Removes Azure Blueprint and OpenShift template support These functionalities are deprecated and not used in the platform anymore. They are full removed from the backend and not supported anymore via the API. --- platform_config_azure.go | 2 -- platform_config_openshift.go | 1 - platform_properties_openshift.go | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/platform_config_azure.go b/platform_config_azure.go index 966e1bf..c753b16 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -15,8 +15,6 @@ type AzureReplicationConfig struct { B2bUserInvitation *AzureInviteB2BUserConfig `json:"b2bUserInvitation,omitempty" tfsdk:"b2b_user_invitation"` SubscriptionNamePattern string `json:"subscriptionNamePattern" tfsdk:"subscription_name_pattern"` GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - BlueprintServicePrincipal string `json:"blueprintServicePrincipal" tfsdk:"blueprint_service_principal"` - BlueprintLocation string `json:"blueprintLocation" tfsdk:"blueprint_location"` AzureRoleMappings types.Set[AzureRoleMapping] `json:"azureRoleMappings" tfsdk:"azure_role_mappings"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` UserLookUpStrategy string `json:"userLookUpStrategy" tfsdk:"user_lookup_strategy"` diff --git a/platform_config_openshift.go b/platform_config_openshift.go index e974b95..e5dfce7 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -13,7 +13,6 @@ type OpenShiftReplicationConfig struct { ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` - EnableTemplateInstantiation bool `json:"enableTemplateInstantiation" tfsdk:"enable_template_instantiation"` OpenshiftRoleMappings types.Set[OpenShiftPlatformRoleMapping] `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` diff --git a/platform_properties_openshift.go b/platform_properties_openshift.go index 15d6752..68a1578 100644 --- a/platform_properties_openshift.go +++ b/platform_properties_openshift.go @@ -1,5 +1,5 @@ package client type OpenShiftPlatformProperties struct { - OpenShiftTemplate *string `json:"openShiftTemplate,omitempty" tfsdk:"openshift_template"` + // Intentionally left empty, as OpenShift platform properties were removed from the meshStack API. } From c6cbc5a68db453a08dba96acbf3afc877030b49c Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 29 Apr 2026 21:36:21 +0200 Subject: [PATCH 130/200] feat: add meshstack_api_key resource --- api_key.go | 61 +++++++++ api_key_permissions.go | 232 +++++++++++++++++++++++++++++++++ api_permissions.go | 86 ------------ client.go | 44 ++++--- client_kind.go | 2 + client_kind_test.go | 1 + internal/mesh_object_client.go | 11 +- platform_config_openshift.go | 12 +- 8 files changed, 332 insertions(+), 117 deletions(-) create mode 100644 api_key.go create mode 100644 api_key_permissions.go delete mode 100644 api_permissions.go diff --git a/api_key.go b/api_key.go new file mode 100644 index 0000000..2747019 --- /dev/null +++ b/api_key.go @@ -0,0 +1,61 @@ +package client + +import ( + "context" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types" +) + +type MeshApiKey struct { + Metadata MeshApiKeyMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshApiKeySpec `json:"spec" tfsdk:"spec"` + Status *MeshApiKeyStatus `json:"status,omitempty" tfsdk:"status"` +} + +type MeshApiKeyMetadata struct { + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshApiKeySpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + Permissions types.Set[ApiPermission] `json:"permissions" tfsdk:"permissions"` + ExpiresAt *string `json:"expiresAt,omitempty" tfsdk:"expires_at"` +} + +type MeshApiKeyStatus struct { + ClientId string `json:"clientId" tfsdk:"client_id"` + ClientSecret *string `json:"clientSecret,omitempty" tfsdk:"client_secret"` +} + +type MeshApiKeyClient interface { + Create(ctx context.Context, apiKey *MeshApiKey) (*MeshApiKey, error) + Read(ctx context.Context, uuid string) (*MeshApiKey, error) + Update(ctx context.Context, uuid string, apiKey *MeshApiKey) (*MeshApiKey, error) + Delete(ctx context.Context, uuid string) error +} + +type meshApiKeyClient struct { + meshObject internal.MeshObjectClient[MeshApiKey] +} + +func newApiKeyClient(ctx context.Context, httpClient *internal.HttpClient) MeshApiKeyClient { + return meshApiKeyClient{internal.NewMeshObjectClient[MeshApiKey](ctx, httpClient, "v1-preview")} +} + +func (c meshApiKeyClient) Create(ctx context.Context, apiKey *MeshApiKey) (*MeshApiKey, error) { + return c.meshObject.Post(ctx, apiKey) +} + +func (c meshApiKeyClient) Read(ctx context.Context, uuid string) (*MeshApiKey, error) { + return c.meshObject.Get(ctx, uuid) +} + +func (c meshApiKeyClient) Update(ctx context.Context, uuid string, apiKey *MeshApiKey) (*MeshApiKey, error) { + return c.meshObject.Put(ctx, uuid, apiKey) +} + +func (c meshApiKeyClient) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) +} diff --git a/api_key_permissions.go b/api_key_permissions.go new file mode 100644 index 0000000..492315b --- /dev/null +++ b/api_key_permissions.go @@ -0,0 +1,232 @@ +package client + +import "strings" + +// API Key Permissions aligned with Kotlin ApiKeyRightMetadataRegistry. +// See https://docs.meshcloud.io/api/authentication/api-permissions/ + +// ApiPermission is a permission shortcode string used for JSON serialization. +type ApiPermission string + +// ApiKeyPermissions is a 3D structure: +// - outer: groups (e.g. "Building Blocks", "Projects") +// - middle: suffix groups within a group (e.g. DELETE, LIST, SAVE variants together) +// - inner: scope variants (e.g. [TENANT_DELETE, ADM_TENANT_DELETE]) +// +// Each permission is listed exactly as it appears in the API, no prefix derivation. +type ApiKeyPermissions [][][]ApiPermission + +// AllCodes returns all valid API key permission shortcodes (flattened). +func (p ApiKeyPermissions) AllCodes() []string { + var codes []string + for _, group := range p { + for _, suffixGroup := range group { + for _, code := range suffixGroup { + codes = append(codes, string(code)) + } + } + } + return codes +} + +// WorkspaceCodes returns only non-ADM_ permission shortcodes (workspace + platform builder scoped). +func (p ApiKeyPermissions) WorkspaceCodes() []string { + var codes []string + for _, group := range p { + for _, suffixGroup := range group { + for _, code := range suffixGroup { + if !strings.HasPrefix(string(code), "ADM_") { + codes = append(codes, string(code)) + } + } + } + } + return codes +} + +// MarkdownString returns an unordered markdown list of all permissions grouped by resource. +// Each bullet shows workspace codes, then MANAGED_ codes, then ADM_ codes separated by " and ". +func (p ApiKeyPermissions) MarkdownString() string { + var lines []string + for _, group := range p { + var workspace, managed, admin []string + for _, suffixGroup := range group { + for _, code := range suffixGroup { + s := string(code) + switch { + case strings.HasPrefix(s, "ADM_"): + admin = append(admin, "`"+s+"`") + case strings.HasPrefix(s, "MANAGED_"): + managed = append(managed, "`"+s+"`") + default: + workspace = append(workspace, "`"+s+"`") + } + } + } + + var parts []string + if len(workspace) > 0 { + parts = append(parts, strings.Join(workspace, "/")) + } + if len(managed) > 0 { + parts = append(parts, strings.Join(managed, "/")) + } + if len(admin) > 0 { + parts = append(parts, strings.Join(admin, "/")) + } + lines = append(lines, " - "+strings.Join(parts, " and ")) + } + return "\n" + strings.Join(lines, "\n") + "\n" +} + +// Permissions is the complete registry of API key permissions, +// aligned 1:1 with the Kotlin ApiKeyRightMetadataRegistry. +var Permissions = ApiKeyPermissions{ + // API Keys + { + {"APIKEY_DELETE", "ADM_APIKEY_DELETE"}, + {"APIKEY_LIST", "ADM_APIKEY_LIST"}, + {"APIKEY_SAVE", "ADM_APIKEY_SAVE"}, + }, + // Building Blocks + { + {"BUILDINGBLOCK_DELETE", "ADM_BUILDINGBLOCK_DELETE"}, + {"BUILDINGBLOCK_LIST", "ADM_BUILDINGBLOCK_LIST", "MANAGED_BUILDINGBLOCK_LIST"}, + {"BUILDINGBLOCK_SAVE", "ADM_BUILDINGBLOCK_SAVE"}, + }, + // Building Block Definitions + { + {"BUILDINGBLOCKDEFINITION_DELETE", "ADM_BUILDINGBLOCKDEFINITION_DELETE"}, + {"BUILDINGBLOCKDEFINITION_LIST", "ADM_BUILDINGBLOCKDEFINITION_LIST"}, + {"BUILDINGBLOCKDEFINITION_SAVE", "ADM_BUILDINGBLOCKDEFINITION_SAVE"}, + {"ADM_REVIEW_PUBLICATION"}, + }, + // Building Block Runs + { + {"MANAGED_BUILDINGBLOCKRUN_LIST", "ADM_BUILDINGBLOCKRUN_LIST"}, + {"MANAGED_BUILDINGBLOCKRUN_SAVE", "ADM_BUILDINGBLOCKRUN_SAVE"}, + {"MANAGED_BUILDINGBLOCKRUNSOURCE_SAVE", "ADM_BUILDINGBLOCKRUNSOURCE_SAVE"}, + }, + // Building Block Runners + { + {"BUILDINGBLOCKRUNNER_DELETE", "ADM_BUILDINGBLOCKRUNNER_DELETE"}, + {"BUILDINGBLOCKRUNNER_LIST", "ADM_BUILDINGBLOCKRUNNER_LIST"}, + {"BUILDINGBLOCKRUNNER_SAVE", "ADM_BUILDINGBLOCKRUNNER_SAVE"}, + }, + // Communication Definitions + { + {"COMMUNICATIONDEFINITION_DELETE", "ADM_COMMUNICATIONDEFINITION_DELETE"}, + {"COMMUNICATIONDEFINITION_LIST", "ADM_COMMUNICATIONDEFINITION_LIST"}, + {"COMMUNICATIONDEFINITION_SAVE", "ADM_COMMUNICATIONDEFINITION_SAVE"}, + }, + // Communications + { + {"COMMUNICATION_DELETE", "ADM_COMMUNICATION_DELETE"}, + {"COMMUNICATION_LIST", "ADM_COMMUNICATION_LIST"}, + {"COMMUNICATION_SAVE", "ADM_COMMUNICATION_SAVE"}, + }, + // Event Logs + { + {"EVENTLOG_LIST", "ADM_EVENTLOG_LIST"}, + }, + // Integrations + { + {"INTEGRATION_DELETE", "ADM_INTEGRATION_DELETE"}, + {"INTEGRATION_LIST", "ADM_INTEGRATION_LIST"}, + {"INTEGRATION_SAVE", "ADM_INTEGRATION_SAVE"}, + }, + // Landing Zones + { + {"LANDINGZONE_DELETE", "ADM_LANDINGZONE_DELETE"}, + {"LANDINGZONE_LIST", "ADM_LANDINGZONE_LIST"}, + {"LANDINGZONE_SAVE", "ADM_LANDINGZONE_SAVE"}, + }, + // Payment Methods + { + {"ADM_PAYMENTMETHOD_DELETE"}, + {"PAYMENTMETHOD_LIST", "ADM_PAYMENTMETHOD_LIST"}, + {"ADM_PAYMENTMETHOD_SAVE"}, + }, + // Platform Instances, Platform Types, Locations + { + {"PLATFORMINSTANCE_DELETE", "ADM_PLATFORMINSTANCE_DELETE"}, + {"PLATFORMINSTANCE_LIST", "ADM_PLATFORMINSTANCE_LIST"}, + {"PLATFORMINSTANCE_SAVE", "ADM_PLATFORMINSTANCE_SAVE"}, + }, + // Project Role Bindings + { + {"PROJECTPRINCIPALROLE_DELETE", "ADM_PROJECTPRINCIPALROLE_DELETE"}, + {"PROJECTPRINCIPALROLE_LIST", "ADM_PROJECTPRINCIPALROLE_LIST"}, + {"PROJECTPRINCIPALROLE_SAVE", "ADM_PROJECTPRINCIPALROLE_SAVE"}, + }, + // Project Roles + { + {"ADM_PROJECTROLE_DELETE"}, + {"ADM_PROJECTROLE_SAVE"}, + }, + // Projects + { + {"PROJECT_DELETE", "ADM_PROJECT_DELETE"}, + {"PROJECT_LIST", "ADM_PROJECT_LIST"}, + {"PROJECT_SAVE", "ADM_PROJECT_SAVE"}, + }, + // Service Instances + { + {"SERVICEINSTANCE_DELETE", "ADM_SERVICEINSTANCE_DELETE"}, + {"SERVICEINSTANCE_LIST", "ADM_SERVICEINSTANCE_LIST"}, + {"SERVICEINSTANCE_SAVE", "ADM_SERVICEINSTANCE_SAVE"}, + }, + // Tag Definitions + { + {"ADM_TAGDEFINITION_DELETE"}, + {"ADM_TAGDEFINITION_LIST"}, + {"ADM_TAGDEFINITION_SAVE"}, + }, + // Tenants + { + {"TENANT_DELETE", "ADM_TENANT_DELETE"}, + {"MANAGED_TENANT_IMPORT", "ADM_TENANT_IMPORT"}, + {"TENANT_LIST", "ADM_TENANT_LIST"}, + {"TENANT_SAVE", "ADM_TENANT_SAVE"}, + }, + // Terraform States + { + {"TFSTATE_DELETE", "ADM_TFSTATE_DELETE", "MANAGED_TFSTATE_DELETE"}, + {"TFSTATE_LIST", "ADM_TFSTATE_LIST", "MANAGED_TFSTATE_LIST"}, + {"TFSTATE_SAVE", "ADM_TFSTATE_SAVE", "MANAGED_TFSTATE_SAVE"}, + }, + // Users + { + {"ADM_USER_DELETE"}, + {"ADM_USER_LIST"}, + {"ADM_USER_SAVE"}, + }, + // Workspace Role Bindings + { + {"WORKSPACEPRINCIPALBINDING_DELETE", "ADM_WORKSPACEPRINCIPALBINDING_DELETE"}, + {"WORKSPACEPRINCIPALBINDING_LIST", "ADM_WORKSPACEPRINCIPALBINDING_LIST"}, + {"WORKSPACEPRINCIPALBINDING_SAVE", "ADM_WORKSPACEPRINCIPALBINDING_SAVE"}, + }, + // Workspace User Groups + { + {"WORKSPACEUSERGROUP_LIST", "ADM_WORKSPACEUSERGROUP_LIST"}, + }, + // Workspaces + { + {"WORKSPACE_DELETE", "ADM_WORKSPACE_DELETE"}, + {"WORKSPACE_LIST", "ADM_WORKSPACE_LIST"}, + {"WORKSPACE_SAVE", "ADM_WORKSPACE_SAVE"}, + }, +} + +// Convenience functions used by consumers. + +// AllApiKeyPermissions returns all valid API key permission shortcodes. +func AllApiKeyPermissions() []string { + return Permissions.AllCodes() +} + +// WorkspacePermissionCodes returns only workspace-scoped permission shortcodes. +func WorkspacePermissionCodes() []string { + return Permissions.WorkspaceCodes() +} diff --git a/api_permissions.go b/api_permissions.go deleted file mode 100644 index e7e89ed..0000000 --- a/api_permissions.go +++ /dev/null @@ -1,86 +0,0 @@ -package client - -import ( - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" -) - -// API Permissions as defined in https://docs.meshcloud.io/api/authentication/api-permissions/ - -type ApiPermission string - -// Workspace Permissions (non-admin). -var ( - WorkspacePermissions = enum.Enum[ApiPermission]{} - - PermissionBuildingBlockDefinitionDelete = WorkspacePermissions.Entry("BUILDINGBLOCKDEFINITION_DELETE") - PermissionBuildingBlockDefinitionList = WorkspacePermissions.Entry("BUILDINGBLOCKDEFINITION_LIST") - PermissionBuildingBlockDefinitionSave = WorkspacePermissions.Entry("BUILDINGBLOCKDEFINITION_SAVE") - - PermissionBuildingBlockRunnerDelete = WorkspacePermissions.Entry("BUILDINGBLOCKRUNNER_DELETE") - PermissionBuildingBlockRunnerList = WorkspacePermissions.Entry("BUILDINGBLOCKRUNNER_LIST") - PermissionBuildingBlockRunnerSave = WorkspacePermissions.Entry("BUILDINGBLOCKRUNNER_SAVE") - - PermissionBuildingBlockDelete = WorkspacePermissions.Entry("BUILDINGBLOCK_DELETE") - PermissionBuildingBlockList = WorkspacePermissions.Entry("BUILDINGBLOCK_LIST") - PermissionBuildingBlockSave = WorkspacePermissions.Entry("BUILDINGBLOCK_SAVE") - - PermissionCommunicationDefinitionDelete = WorkspacePermissions.Entry("COMMUNICATIONDEFINITION_DELETE") - PermissionCommunicationDefinitionList = WorkspacePermissions.Entry("COMMUNICATIONDEFINITION_LIST") - PermissionCommunicationDefinitionSave = WorkspacePermissions.Entry("COMMUNICATIONDEFINITION_SAVE") - - PermissionCommunicationDelete = WorkspacePermissions.Entry("COMMUNICATION_DELETE") - PermissionCommunicationList = WorkspacePermissions.Entry("COMMUNICATION_LIST") - PermissionCommunicationSave = WorkspacePermissions.Entry("COMMUNICATION_SAVE") - - PermissionEventLogList = WorkspacePermissions.Entry("EVENTLOG_LIST") - - PermissionIntegrationDelete = WorkspacePermissions.Entry("INTEGRATION_DELETE") - PermissionIntegrationList = WorkspacePermissions.Entry("INTEGRATION_LIST") - PermissionIntegrationSave = WorkspacePermissions.Entry("INTEGRATION_SAVE") - - PermissionLandingZoneDelete = WorkspacePermissions.Entry("LANDINGZONE_DELETE") - PermissionLandingZoneList = WorkspacePermissions.Entry("LANDINGZONE_LIST") - PermissionLandingZoneSave = WorkspacePermissions.Entry("LANDINGZONE_SAVE") - - PermissionManagedBuildingBlockRunSourceSave = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCKRUNSOURCE_SAVE") - PermissionManagedBuildingBlockRunList = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCKRUN_LIST") - PermissionManagedBuildingBlockRunSave = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCKRUN_SAVE") - PermissionManagedBuildingBlockList = WorkspacePermissions.Entry("MANAGED_BUILDINGBLOCK_LIST") - PermissionManagedTenantImport = WorkspacePermissions.Entry("MANAGED_TENANT_IMPORT") - - PermissionPaymentMethodList = WorkspacePermissions.Entry("PAYMENTMETHOD_LIST") - - PermissionPlatformInstanceDelete = WorkspacePermissions.Entry("PLATFORMINSTANCE_DELETE") - PermissionPlatformInstanceList = WorkspacePermissions.Entry("PLATFORMINSTANCE_LIST") - PermissionPlatformInstanceSave = WorkspacePermissions.Entry("PLATFORMINSTANCE_SAVE") - - PermissionProjectPrincipalRoleDelete = WorkspacePermissions.Entry("PROJECTPRINCIPALROLE_DELETE") - PermissionProjectPrincipalRoleList = WorkspacePermissions.Entry("PROJECTPRINCIPALROLE_LIST") - PermissionProjectPrincipalRoleSave = WorkspacePermissions.Entry("PROJECTPRINCIPALROLE_SAVE") - - PermissionProjectDelete = WorkspacePermissions.Entry("PROJECT_DELETE") - PermissionProjectList = WorkspacePermissions.Entry("PROJECT_LIST") - PermissionProjectSave = WorkspacePermissions.Entry("PROJECT_SAVE") - - PermissionServiceInstanceDelete = WorkspacePermissions.Entry("SERVICEINSTANCE_DELETE") - PermissionServiceInstanceList = WorkspacePermissions.Entry("SERVICEINSTANCE_LIST") - PermissionServiceInstanceSave = WorkspacePermissions.Entry("SERVICEINSTANCE_SAVE") - - PermissionTenantDelete = WorkspacePermissions.Entry("TENANT_DELETE") - PermissionTenantList = WorkspacePermissions.Entry("TENANT_LIST") - PermissionTenantSave = WorkspacePermissions.Entry("TENANT_SAVE") - - PermissionTfStateDelete = WorkspacePermissions.Entry("TFSTATE_DELETE") - PermissionTfStateList = WorkspacePermissions.Entry("TFSTATE_LIST") - PermissionTfStateSave = WorkspacePermissions.Entry("TFSTATE_SAVE") - - PermissionWorkspacePrincipalBindingDelete = WorkspacePermissions.Entry("WORKSPACEPRINCIPALBINDING_DELETE") - PermissionWorkspacePrincipalBindingList = WorkspacePermissions.Entry("WORKSPACEPRINCIPALBINDING_LIST") - PermissionWorkspacePrincipalBindingSave = WorkspacePermissions.Entry("WORKSPACEPRINCIPALBINDING_SAVE") - - PermissionWorkspaceUserGroupList = WorkspacePermissions.Entry("WORKSPACEUSERGROUP_LIST") - - PermissionWorkspaceDelete = WorkspacePermissions.Entry("WORKSPACE_DELETE") - PermissionWorkspaceList = WorkspacePermissions.Entry("WORKSPACE_LIST") - PermissionWorkspaceSave = WorkspacePermissions.Entry("WORKSPACE_SAVE") -) diff --git a/client.go b/client.go index a3c8eef..010c1eb 100644 --- a/client.go +++ b/client.go @@ -17,6 +17,7 @@ import ( var MinMeshStackVersion = version.MustParse("2026.10.0") type Client struct { + ApiKey MeshApiKeyClient BuildingBlock MeshBuildingBlockClient BuildingBlockV2 MeshBuildingBlockV2Client BuildingBlockDefinition MeshBuildingBlockDefinitionClient @@ -26,6 +27,7 @@ type Client struct { Location MeshLocationClient PaymentMethod MeshPaymentMethodClient Platform MeshPlatformClient + PlatformType MeshPlatformTypeClient Project MeshProjectClient ProjectGroupBinding MeshProjectGroupBindingClient ProjectUserBinding MeshProjectUserBindingClient @@ -36,7 +38,6 @@ type Client struct { Workspace MeshWorkspaceClient WorkspaceGroupBinding MeshWorkspaceGroupBindingClient WorkspaceUserBinding MeshWorkspaceUserBindingClient - PlatformType MeshPlatformTypeClient } func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string, apiToken string) (Client, error) { @@ -70,26 +71,27 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str } return Client{ - newBuildingBlockClient(ctx, httpClient), - newBuildingBlockV2Client(ctx, httpClient), - newBuildingBlockDefinitionClient(ctx, httpClient), - newBuildingBlockDefinitionVersionClient(ctx, httpClient), - newIntegrationClient(ctx, httpClient), - newLandingZoneClient(ctx, httpClient), - newLocationClient(ctx, httpClient), - newPaymentMethodClient(ctx, httpClient), - newPlatformClient(ctx, httpClient), - newProjectClient(ctx, httpClient), - newProjectGroupBindingClient(ctx, httpClient), - newProjectUserBindingClient(ctx, httpClient), - newServiceInstanceClient(ctx, httpClient), - newTagDefinitionClient(ctx, httpClient), - newTenantClient(ctx, httpClient), - newTenantV4Client(ctx, httpClient), - newWorkspaceClient(ctx, httpClient), - newWorkspaceGroupBindingClient(ctx, httpClient), - newWorkspaceUserBindingClient(ctx, httpClient), - newPlatformTypeClient(ctx, httpClient), + ApiKey: newApiKeyClient(ctx, httpClient), + BuildingBlock: newBuildingBlockClient(ctx, httpClient), + BuildingBlockV2: newBuildingBlockV2Client(ctx, httpClient), + BuildingBlockDefinition: newBuildingBlockDefinitionClient(ctx, httpClient), + BuildingBlockDefinitionVersion: newBuildingBlockDefinitionVersionClient(ctx, httpClient), + Integration: newIntegrationClient(ctx, httpClient), + LandingZone: newLandingZoneClient(ctx, httpClient), + Location: newLocationClient(ctx, httpClient), + PaymentMethod: newPaymentMethodClient(ctx, httpClient), + Platform: newPlatformClient(ctx, httpClient), + PlatformType: newPlatformTypeClient(ctx, httpClient), + Project: newProjectClient(ctx, httpClient), + ProjectGroupBinding: newProjectGroupBindingClient(ctx, httpClient), + ProjectUserBinding: newProjectUserBindingClient(ctx, httpClient), + ServiceInstance: newServiceInstanceClient(ctx, httpClient), + TagDefinition: newTagDefinitionClient(ctx, httpClient), + Tenant: newTenantClient(ctx, httpClient), + TenantV4: newTenantV4Client(ctx, httpClient), + Workspace: newWorkspaceClient(ctx, httpClient), + WorkspaceGroupBinding: newWorkspaceGroupBindingClient(ctx, httpClient), + WorkspaceUserBinding: newWorkspaceUserBindingClient(ctx, httpClient), }, nil } diff --git a/client_kind.go b/client_kind.go index 340a02f..6bc91fa 100644 --- a/client_kind.go +++ b/client_kind.go @@ -2,6 +2,7 @@ package client // meshObjectKind provides typed constants for meshObject kind strings used across the provider. type meshObjectKind struct { + ApiKey string BuildingBlock string BuildingBlockDefinition string BuildingBlockDefinitionVersion string @@ -25,6 +26,7 @@ type meshObjectKind struct { } var MeshObjectKind = meshObjectKind{ + ApiKey: "meshApiKey", BuildingBlock: "meshBuildingBlock", BuildingBlockDefinition: "meshBuildingBlockDefinition", BuildingBlockDefinitionVersion: "meshBuildingBlockDefinitionVersion", diff --git a/client_kind_test.go b/client_kind_test.go index f011b00..328acad 100644 --- a/client_kind_test.go +++ b/client_kind_test.go @@ -10,6 +10,7 @@ import ( func TestKind(t *testing.T) { // verify hardcoded kind strings match InferKind for all client types + assert.Equal(t, internal.InferKind[MeshApiKey](), MeshObjectKind.ApiKey) assert.Equal(t, internal.InferKind[MeshBuildingBlock](), MeshObjectKind.BuildingBlock) assert.Equal(t, internal.InferKind[MeshBuildingBlockV2](), MeshObjectKind.BuildingBlock) assert.Equal(t, internal.InferKind[MeshBuildingBlockDefinition](), MeshObjectKind.BuildingBlockDefinition) diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index dcd65b8..2a6ff1d 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -59,12 +59,15 @@ func InferKind[M any]() string { return versionSuffixRe.ReplaceAllString(kind, "") } +var pluralExceptions = map[string]string{ + // Add exceptions here as needed, e.g. "meshPolicy": "meshPolicies" +} + func pluralizeKind(kind string) string { - if strings.HasSuffix(kind, "y") { - // this is ok, as we don't have meshObjects ending in 'y' yet, so take this shortcut - panic(fmt.Sprintf("Correctly pluralizing meshObject kind '%s' is not supported yet", kind)) + if plural, ok := pluralExceptions[kind]; ok { + return plural } - return fmt.Sprintf("%ss", kind) + return kind + "s" } func (c MeshObjectClient[M]) meshObjectMimeType() string { diff --git a/platform_config_openshift.go b/platform_config_openshift.go index e5dfce7..49587d7 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -10,12 +10,12 @@ type OpenShiftPlatformConfig struct { } type OpenShiftReplicationConfig struct { - ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` - WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` - ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` - OpenshiftRoleMappings types.Set[OpenShiftPlatformRoleMapping] `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` - IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` - TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + ClientConfig KubernetesClientConfig `json:"clientConfig" tfsdk:"client_config"` + WebConsoleUrl *string `json:"webConsoleUrl,omitempty" tfsdk:"web_console_url"` + ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` + OpenshiftRoleMappings types.Set[OpenShiftPlatformRoleMapping] `json:"openshiftRoleMappings" tfsdk:"openshift_role_mappings"` + IdentityProviderName string `json:"identityProviderName" tfsdk:"identity_provider_name"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` } type OpenShiftMeteringConfig struct { From a68437456f3115acf7cceaa304386335e37196bc Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 5 May 2026 12:42:55 +0200 Subject: [PATCH 131/200] test: add cross-workspace BBD listing test --- buildingblock_definition.go | 3 ++- client.go | 4 ++++ internal/http_client.go | 36 ++++++++++++++++++++++------------ internal/mesh_object_client.go | 2 +- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/buildingblock_definition.go b/buildingblock_definition.go index 0262f38..e69ba98 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -82,8 +82,9 @@ func newBuildingBlockDefinitionClient(ctx context.Context, httpClient *internal. func (c meshBuildingBlockDefinitionClient) List(ctx context.Context, workspaceIdentifier *string) ([]MeshBuildingBlockDefinition, error) { var options []internal.RequestOption + options = append(options, internal.WithUrlQuery("includeAllPublished", "true")) if workspaceIdentifier != nil { - options = append(options, internal.WithUrlQuery("workspaceIdentifier", *workspaceIdentifier)) + options = append(options, internal.WithUrlQuery("ownedByWorkspace", *workspaceIdentifier)) } return c.meshObject.List(ctx, options...) } diff --git a/client.go b/client.go index 010c1eb..dc67f41 100644 --- a/client.go +++ b/client.go @@ -16,6 +16,10 @@ import ( var MinMeshStackVersion = version.MustParse("2026.10.0") +// HttpError represents an HTTP error response with status code. +// This error is returned when an HTTP request fails with a non-2XX status code. +type HttpError = internal.HttpError + type Client struct { ApiKey MeshApiKeyClient BuildingBlock MeshBuildingBlockClient diff --git a/internal/http_client.go b/internal/http_client.go index 0fee9d5..eeb1e4f 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -15,9 +14,26 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var ( - errNotFound = errors.New("request failed with status Not Found (404)") -) +// HttpError represents an HTTP error response with status code. +// This error is returned when an HTTP request fails with a non-2XX status code. +type HttpError struct { + StatusCode int + Message string +} + +func (e HttpError) Error() string { + return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message) +} + +// IsForbidden returns true if the error is a 403 Forbidden response. +func (e HttpError) IsForbidden() bool { + return e.StatusCode == http.StatusForbidden +} + +// IsNotFound returns true if the error is a 404 Not Found response. +func (e HttpError) IsNotFound() bool { + return e.StatusCode == http.StatusNotFound +} // HttpClient wraps [http.Client] with convenient request handling thanks to RequestOption. type HttpClient struct { @@ -63,15 +79,11 @@ func (c *HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Resp if res.StatusCode >= 200 && res.StatusCode <= 299 { return responseBody, nil } - var errs []error - if res.StatusCode == http.StatusNotFound { - errs = append(errs, errNotFound) + + return responseBody, HttpError{ + StatusCode: res.StatusCode, + Message: string(responseBody), } - errs = append(errs, - fmt.Errorf("request failed with status %d (not 2XX successful)", res.StatusCode), - fmt.Errorf("error response: %s", string(responseBody)), - ) - return responseBody, errors.Join(errs...) } func (c *HttpClient) buildRequest(ctx context.Context, method string, url url.URL, opts requestOptions) (*http.Request, error) { diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 2a6ff1d..92f027c 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -77,7 +77,7 @@ func (c MeshObjectClient[M]) meshObjectMimeType() string { // Get retrieves a meshObject by ID. Returns nil if not found. func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (*M, error) { body, err := c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) - if errors.Is(err, errNotFound) { + if httpErr, ok := errors.AsType[HttpError](err); ok && httpErr.IsNotFound() { return nil, nil } return unmarshalBody[M](body, err) From afbfed3cc1aa9b889640ed9563940fc6f6d6dd3f Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 8 May 2026 12:50:05 +0200 Subject: [PATCH 132/200] refactor: use Authoriztion.Header abstraction, improve error message --- api_key.go | 2 +- buildingblock.go | 2 +- buildingblock_definition.go | 2 +- buildingblock_definition_version.go | 2 +- buildingblock_v2.go | 2 +- client.go | 59 ++++++------------------ client_test.go | 69 ---------------------------- integration.go | 4 +- internal/auth.go | 71 +++++++++++++++++++++++++++++ internal/http_client.go | 54 ++++++++-------------- internal/http_error.go | 27 +++++++++++ internal/mesh_object_client.go | 61 ++++--------------------- landingzone.go | 2 +- location.go | 2 +- payment_method.go | 2 +- platform.go | 2 +- platform_type.go | 2 +- project.go | 2 +- project_group_binding.go | 2 +- project_user_binding.go | 2 +- service_instance.go | 2 +- tag_definition.go | 2 +- tenant.go | 2 +- tenant_v4.go | 2 +- workspace.go | 2 +- workspace_group_binding.go | 2 +- workspace_user_binding.go | 2 +- 27 files changed, 162 insertions(+), 223 deletions(-) delete mode 100644 client_test.go create mode 100644 internal/auth.go create mode 100644 internal/http_error.go diff --git a/api_key.go b/api_key.go index 2747019..ec99890 100644 --- a/api_key.go +++ b/api_key.go @@ -40,7 +40,7 @@ type meshApiKeyClient struct { meshObject internal.MeshObjectClient[MeshApiKey] } -func newApiKeyClient(ctx context.Context, httpClient *internal.HttpClient) MeshApiKeyClient { +func newApiKeyClient(ctx context.Context, httpClient internal.HttpClient) MeshApiKeyClient { return meshApiKeyClient{internal.NewMeshObjectClient[MeshApiKey](ctx, httpClient, "v1-preview")} } diff --git a/buildingblock.go b/buildingblock.go index 97ecff7..fa93b54 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -82,7 +82,7 @@ type meshBuildingBlockClient struct { meshObject internal.MeshObjectClient[MeshBuildingBlock] } -func newBuildingBlockClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockClient { +func newBuildingBlockClient(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockClient { return meshBuildingBlockClient{internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1")} } diff --git a/buildingblock_definition.go b/buildingblock_definition.go index e69ba98..667f5fb 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -74,7 +74,7 @@ type meshBuildingBlockDefinitionClient struct { meshObject internal.MeshObjectClient[MeshBuildingBlockDefinition] } -func newBuildingBlockDefinitionClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockDefinitionClient { +func newBuildingBlockDefinitionClient(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockDefinitionClient { return meshBuildingBlockDefinitionClient{ meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinition](ctx, httpClient, "v1-preview"), } diff --git a/buildingblock_definition_version.go b/buildingblock_definition_version.go index 77bd0ed..72c38aa 100644 --- a/buildingblock_definition_version.go +++ b/buildingblock_definition_version.go @@ -197,7 +197,7 @@ type meshBuildingBlockDefinitionVersionClient struct { meshObject internal.MeshObjectClient[MeshBuildingBlockDefinitionVersion] } -func newBuildingBlockDefinitionVersionClient(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockDefinitionVersionClient { +func newBuildingBlockDefinitionVersionClient(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockDefinitionVersionClient { return meshBuildingBlockDefinitionVersionClient{ meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinitionVersion](ctx, httpClient, "v1-preview"), } diff --git a/buildingblock_v2.go b/buildingblock_v2.go index db09c73..66ba7ae 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -71,7 +71,7 @@ type meshBuildingBlockV2Client struct { meshObject internal.MeshObjectClient[MeshBuildingBlockV2] } -func newBuildingBlockV2Client(ctx context.Context, httpClient *internal.HttpClient) MeshBuildingBlockV2Client { +func newBuildingBlockV2Client(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockV2Client { return meshBuildingBlockV2Client{internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview")} } diff --git a/client.go b/client.go index dc67f41..aabaccc 100644 --- a/client.go +++ b/client.go @@ -2,12 +2,9 @@ package client import ( "context" - "encoding/base64" - "encoding/json" "fmt" "net/http" "net/url" - "strings" "time" "github.com/meshcloud/terraform-provider-meshstack/client/internal" @@ -44,27 +41,22 @@ type Client struct { WorkspaceUserBinding MeshWorkspaceUserBindingClient } -func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret string, apiToken string) (Client, error) { - httpClient := &internal.HttpClient{ - Client: http.Client{Timeout: 5 * time.Minute}, - RootUrl: rootUrl, - UserAgent: userAgent, +type Authorization = internal.Authorization - // Putting authentication with meshStack API into HttpClient - // saves use from passing ApiKey/ApiSecret down to client factory methods below. - ApiKey: apiKey, - ApiSecret: apiSecret, - } +func NewApiTokenAuthorization(apiToken string) Authorization { + return internal.BearerTokenAuthorization{Token: apiToken} +} - if apiToken != "" { - httpClient.Authorization = "Bearer " + apiToken +func NewApiKeyAuthorization(apiKey, apiSecret string) Authorization { + return internal.NewClientSecretAuthorization("api/login", apiKey, apiSecret) +} - if expiresAt, err := parseTokenExpiration(apiToken); err == nil { - httpClient.AuthorizationExpiresAt = expiresAt - } else { - // If token has no expiration we assume it is valid for the default duration. - httpClient.AuthorizationExpiresAt = time.Now().Add(6 * time.Hour) - } +func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authorization) (Client, error) { + httpClient := internal.HttpClient{ + Client: &http.Client{Timeout: 5 * time.Minute}, + RootUrl: rootUrl, + UserAgent: userAgent, + Authorization: auth, } // Check meshStack version compatibility @@ -98,28 +90,3 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent, apiKey, apiSecret str WorkspaceUserBinding: newWorkspaceUserBindingClient(ctx, httpClient), }, nil } - -func parseTokenExpiration(token string) (time.Time, error) { - parts := strings.Split(token, ".") - if len(parts) != 3 { - return time.Time{}, fmt.Errorf("invalid token format") - } - - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return time.Time{}, err - } - - var claims struct { - Exp int64 `json:"exp"` - } - if err := json.Unmarshal(payload, &claims); err != nil { - return time.Time{}, err - } - - if claims.Exp == 0 { - return time.Time{}, fmt.Errorf("expiration claim missing") - } - - return time.Unix(claims.Exp, 0), nil -} diff --git a/client_test.go b/client_test.go deleted file mode 100644 index 0fed152..0000000 --- a/client_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package client - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParseTokenExpiration(t *testing.T) { - // Helper to create a dummy JWT with a specific expiration time - createToken := func(expTime time.Time) string { - header := `{"alg":"HS256","typ":"JWT"}` - payload := map[string]any{ - "sub": "1234567890", - "name": "John Doe", - "exp": expTime.Unix(), - } - - payloadBytes, _ := json.Marshal(payload) - - encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(header)) - encodedPayload := base64.RawURLEncoding.EncodeToString(payloadBytes) - signature := "dummy_signature" - - return fmt.Sprintf("%s.%s.%s", encodedHeader, encodedPayload, signature) - } - - fixedBaseTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) - - t.Run("Valid token", func(t *testing.T) { - expTime := fixedBaseTime - token := createToken(expTime) - - parsedTime, err := parseTokenExpiration(token) - - require.NoError(t, err) - assert.Equal(t, expTime.Unix(), parsedTime.Unix()) - }) - - t.Run("Invalid format - not enough parts", func(t *testing.T) { - token := "invalid.token" - _, err := parseTokenExpiration(token) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid token format") - }) - - t.Run("Invalid Base64 payload", func(t *testing.T) { - token := "header.invalid_base64$.signature" - _, err := parseTokenExpiration(token) - assert.Error(t, err) - }) - - t.Run("Missing exp claim", func(t *testing.T) { - header := `{"alg":"HS256","typ":"JWT"}` - payload := `{"sub":"1234567890"}` // No exp - encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(header)) - encodedPayload := base64.RawURLEncoding.EncodeToString([]byte(payload)) - token := fmt.Sprintf("%s.%s.sig", encodedHeader, encodedPayload) - - _, err := parseTokenExpiration(token) - require.Error(t, err) - assert.Contains(t, err.Error(), "expiration claim missing") - }) -} diff --git a/integration.go b/integration.go index 78cbdba..1753647 100644 --- a/integration.go +++ b/integration.go @@ -56,8 +56,8 @@ type meshIntegrationClientImpl struct { meshObject internal.MeshObjectClient[MeshIntegration] } -func newIntegrationClient(ctx context.Context, httpClient *internal.HttpClient) MeshIntegrationClient { - return &meshIntegrationClientImpl{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1")} +func newIntegrationClient(ctx context.Context, httpClient internal.HttpClient) MeshIntegrationClient { + return meshIntegrationClientImpl{internal.NewMeshObjectClient[MeshIntegration](ctx, httpClient, "v1")} } func (c meshIntegrationClientImpl) Create(ctx context.Context, integration MeshIntegration) (*MeshIntegration, error) { diff --git a/internal/auth.go b/internal/auth.go new file mode 100644 index 0000000..4e0db61 --- /dev/null +++ b/internal/auth.go @@ -0,0 +1,71 @@ +package internal + +import ( + "context" + "fmt" + "time" +) + +type Authorization interface { + Header(ctx context.Context, client HttpClient) (string, error) +} + +func NewClientSecretAuthorization(loginApiPath, clientId, clientSecret string) Authorization { + return &clientSecretAuthorization{ + BearerTokenAuthorization{}, // empty token initially, is refreshed on demand in ensureValidToken + loginApiPath, + clientId, clientSecret, + time.Time{}, // expiry also set in ensureValidToken + } +} + +type BearerTokenAuthorization struct { + Token string +} + +func (auth BearerTokenAuthorization) Header(_ context.Context, _ HttpClient) (string, error) { + return fmt.Sprintf("Bearer %s", auth.Token), nil +} + +type clientSecretAuthorization struct { + BearerTokenAuthorization + LoginApiPath string + ClientId string + ClientSecret string + ExpiresAt time.Time +} + +func (auth *clientSecretAuthorization) Header(ctx context.Context, client HttpClient) (string, error) { + if err := auth.ensureValidToken(ctx, client); err != nil { + return "", err + } + return auth.BearerTokenAuthorization.Header(ctx, client) +} + +func (auth *clientSecretAuthorization) ensureValidToken(ctx context.Context, client HttpClient) error { + if auth.Token != "" && time.Until(auth.ExpiresAt) > 30*time.Second { + return nil + } + + loginApiUrl := client.RootUrl.JoinPath(auth.LoginApiPath) + + type loginRequest struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + } + + type loginResponse struct { + Token string `json:"access_token"` + ExpireSec int `json:"expires_in"` + } + + loginResult, err := unmarshalBody[loginResponse](client.doRequest(ctx, "POST", loginApiUrl, + withPayload(loginRequest{ClientId: auth.ClientId, ClientSecret: auth.ClientSecret}, "application/json")), + ) + if err != nil { + return fmt.Errorf("login at %s with client id '%s' failed: %w", loginApiUrl, auth.ClientId, err) + } + auth.Token = loginResult.Token + auth.ExpiresAt = time.Now().Add(time.Duration(loginResult.ExpireSec) * time.Second) + return nil +} diff --git a/internal/http_client.go b/internal/http_client.go index eeb1e4f..d6c22b5 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -9,45 +9,19 @@ import ( "net/http" "net/url" "slices" - "time" "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -// HttpError represents an HTTP error response with status code. -// This error is returned when an HTTP request fails with a non-2XX status code. -type HttpError struct { - StatusCode int - Message string -} - -func (e HttpError) Error() string { - return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message) -} - -// IsForbidden returns true if the error is a 403 Forbidden response. -func (e HttpError) IsForbidden() bool { - return e.StatusCode == http.StatusForbidden -} - -// IsNotFound returns true if the error is a 404 Not Found response. -func (e HttpError) IsNotFound() bool { - return e.StatusCode == http.StatusNotFound -} - // HttpClient wraps [http.Client] with convenient request handling thanks to RequestOption. type HttpClient struct { - http.Client - RootUrl *url.URL - UserAgent string - - ApiKey string - ApiSecret string - Authorization string - AuthorizationExpiresAt time.Time + *http.Client + RootUrl *url.URL + UserAgent string + Authorization Authorization } -func (c *HttpClient) doRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { +func (c HttpClient) doRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { options = slices.Insert(options, 0, withHeader("User-Agent", c.UserAgent), ) @@ -69,7 +43,15 @@ func (c *HttpClient) doRequest(ctx context.Context, method string, url *url.URL, return c.readBodyAndCheckSuccess(ctx, res) } -func (c *HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Response) ([]byte, error) { +func (c HttpClient) doAuthorizedRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { + authHeader, err := c.Authorization.Header(ctx, c) + if err != nil { + return nil, err + } + return c.doRequest(ctx, method, url, append(options, withHeader("Authorization", authHeader))...) +} + +func (c HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Response) ([]byte, error) { responseBody, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("cannot read response body, status code %d: %w", res.StatusCode, err) @@ -86,7 +68,7 @@ func (c *HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Resp } } -func (c *HttpClient) buildRequest(ctx context.Context, method string, url url.URL, opts requestOptions) (*http.Request, error) { +func (c HttpClient) buildRequest(ctx context.Context, method string, url url.URL, opts requestOptions) (*http.Request, error) { if len(opts.urlQueryParams) > 0 { query := url.Query() for k, v := range opts.urlQueryParams { @@ -114,13 +96,15 @@ func (c *HttpClient) buildRequest(ctx context.Context, method string, url url.UR return req, err } +// unmarshalBody is a generic helper to unmarshal a JSON response. +// It intentionally takes err as second argument to match doAuthorizedRequest and doRequest signatures. func unmarshalBody[T any](body []byte, err error) (*T, error) { if err != nil { return nil, err } var target T if err := json.Unmarshal(body, &target); err != nil { - return nil, err + return nil, fmt.Errorf("cannot unmarshal body: %w", err) } return &target, nil } @@ -129,7 +113,7 @@ type MeshInfo struct { Version version.Version `json:"version"` } -func (c *HttpClient) GetMeshInfo(ctx context.Context) (*MeshInfo, error) { +func (c HttpClient) GetMeshInfo(ctx context.Context) (*MeshInfo, error) { meshInfoUrl := c.RootUrl.JoinPath("/mesh/info") return unmarshalBody[MeshInfo](c.doRequest(ctx, "GET", meshInfoUrl)) } diff --git a/internal/http_error.go b/internal/http_error.go new file mode 100644 index 0000000..7b5d3e8 --- /dev/null +++ b/internal/http_error.go @@ -0,0 +1,27 @@ +package internal + +import ( + "fmt" + "net/http" +) + +// HttpError represents an HTTP error response with status code. +// This error is returned when an HTTP request fails with a non-2XX status code. +type HttpError struct { + StatusCode int + Message string +} + +func (e HttpError) Error() string { + return fmt.Sprintf("http error %d: %s", e.StatusCode, e.Message) +} + +// IsForbidden returns true if the error is a 403 Forbidden response. +func (e HttpError) IsForbidden() bool { + return e.StatusCode == http.StatusForbidden +} + +// IsNotFound returns true if the error is a 404 Not Found response. +func (e HttpError) IsNotFound() bool { + return e.StatusCode == http.StatusNotFound +} diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 92f027c..024ab4e 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -11,7 +11,6 @@ import ( "regexp" "slices" "strings" - "time" "unicode" ) @@ -21,7 +20,7 @@ import ( // Also handles authentication in doAuthorizedRequest using the ApiKey/ApiSecret values, // which are embedded in HttpClient for convenient construction with NewMeshObjectClient. type MeshObjectClient[M any] struct { - *HttpClient + HttpClient Kind string ApiVersion string ApiUrl *url.URL @@ -31,7 +30,7 @@ type MeshObjectClient[M any] struct { // The meshObject kind is inferred from type M. T // The API URL is constructed from explicitApiPathElems if provided, // otherwise the pluralized and lowercased kind is used as a single element. -func NewMeshObjectClient[M any](ctx context.Context, httpClient *HttpClient, apiVersion string, explicitApiPathElems ...string) MeshObjectClient[M] { +func NewMeshObjectClient[M any](ctx context.Context, httpClient HttpClient, apiVersion string, explicitApiPathElems ...string) MeshObjectClient[M] { kind := InferKind[M]() if len(explicitApiPathElems) == 0 { @@ -75,12 +74,12 @@ func (c MeshObjectClient[M]) meshObjectMimeType() string { } // Get retrieves a meshObject by ID. Returns nil if not found. -func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (*M, error) { - body, err := c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) +func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (resp *M, err error) { + resp, err = unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType()))) if httpErr, ok := errors.AsType[HttpError](err); ok && httpErr.IsNotFound() { return nil, nil } - return unmarshalBody[M](body, err) + return } // Post creates a new meshObject with the given payload. @@ -132,13 +131,6 @@ func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) pageNumber := 0 for { - body, err := c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl, append(options, - withAccept(c.meshObjectMimeType()), - WithUrlQuery("page", pageNumber), - )...) - if err != nil { - return result, fmt.Errorf("cannot fetch page %d: %w", pageNumber, err) - } type paginatedResponse struct { Embedded map[string][]M `json:"_embedded"` Page struct { @@ -146,9 +138,12 @@ func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) Number int `json:"number"` } `json:"page"` } - response, err := unmarshalBody[paginatedResponse](body, err) + response, err := unmarshalBody[paginatedResponse](c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl, append(options, + withAccept(c.meshObjectMimeType()), + WithUrlQuery("page", pageNumber), + )...)) if err != nil { - return result, fmt.Errorf("cannot unmarshal paginated response, page %d: %w", pageNumber, err) + return result, fmt.Errorf("error getting page %d: %w", pageNumber, err) } else if items, ok := response.Embedded[embeddedKey]; !ok { return result, fmt.Errorf("embedded key %s not found in paginated response", embeddedKey) } else { @@ -160,39 +155,3 @@ func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) pageNumber++ } } - -func (c MeshObjectClient[M]) doAuthorizedRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { - if err := c.ensureAuthorization(ctx); err != nil { - return nil, err - } - return c.doRequest(ctx, method, url, append(options, withHeader("Authorization", c.Authorization))...) -} - -func (c MeshObjectClient[M]) ensureAuthorization(ctx context.Context) error { - if c.Authorization != "" && time.Until(c.AuthorizationExpiresAt) > 30*time.Second { - return nil - } - - loginApiUrl := c.RootUrl.JoinPath("/api/login") - - type loginRequest struct { - ClientId string `json:"clientId"` - ClientSecret string `json:"clientSecret"` - } - - type loginResponse struct { - Token string `json:"access_token"` - ExpireSec int `json:"expires_in"` - } - - loginResult, err := unmarshalBody[loginResponse](c.doRequest(ctx, "POST", loginApiUrl, - withPayload(loginRequest{ClientId: c.ApiKey, ClientSecret: c.ApiSecret}, "application/json")), - ) - if err != nil { - return fmt.Errorf("login request to %s with API Key '%s' failed: %w", loginApiUrl, c.ApiKey, err) - } - - c.Authorization = fmt.Sprintf("Bearer %s", loginResult.Token) - c.AuthorizationExpiresAt = time.Now().Add(time.Duration(loginResult.ExpireSec) * time.Second) - return nil -} diff --git a/landingzone.go b/landingzone.go index 157d164..bf7ac67 100644 --- a/landingzone.go +++ b/landingzone.go @@ -74,7 +74,7 @@ type meshLandingZoneClient struct { meshObject internal.MeshObjectClient[MeshLandingZone] } -func newLandingZoneClient(ctx context.Context, httpClient *internal.HttpClient) MeshLandingZoneClient { +func newLandingZoneClient(ctx context.Context, httpClient internal.HttpClient) MeshLandingZoneClient { return meshLandingZoneClient{internal.NewMeshObjectClient[MeshLandingZone](ctx, httpClient, "v1")} } diff --git a/location.go b/location.go index 33bf0a8..d3e3e0a 100644 --- a/location.go +++ b/location.go @@ -48,7 +48,7 @@ type meshLocationClient struct { meshObject internal.MeshObjectClient[MeshLocation] } -func newLocationClient(ctx context.Context, httpClient *internal.HttpClient) MeshLocationClient { +func newLocationClient(ctx context.Context, httpClient internal.HttpClient) MeshLocationClient { return meshLocationClient{internal.NewMeshObjectClient[MeshLocation](ctx, httpClient, "v1")} } diff --git a/payment_method.go b/payment_method.go index 6867053..f32ffc1 100644 --- a/payment_method.go +++ b/payment_method.go @@ -46,7 +46,7 @@ type meshPaymentMethodClient struct { meshObject internal.MeshObjectClient[MeshPaymentMethod] } -func newPaymentMethodClient(ctx context.Context, httpClient *internal.HttpClient) MeshPaymentMethodClient { +func newPaymentMethodClient(ctx context.Context, httpClient internal.HttpClient) MeshPaymentMethodClient { return meshPaymentMethodClient{internal.NewMeshObjectClient[MeshPaymentMethod](ctx, httpClient, "v2")} } diff --git a/platform.go b/platform.go index 08623ea..5de34b3 100644 --- a/platform.go +++ b/platform.go @@ -91,7 +91,7 @@ type meshPlatformClient struct { meshObject internal.MeshObjectClient[MeshPlatform] } -func newPlatformClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformClient { +func newPlatformClient(ctx context.Context, httpClient internal.HttpClient) MeshPlatformClient { return meshPlatformClient{internal.NewMeshObjectClient[MeshPlatform](ctx, httpClient, "v2")} } diff --git a/platform_type.go b/platform_type.go index 6160b32..a13a77f 100644 --- a/platform_type.go +++ b/platform_type.go @@ -55,7 +55,7 @@ type meshPlatformTypeClient struct { meshObject internal.MeshObjectClient[MeshPlatformType] } -func newPlatformTypeClient(ctx context.Context, httpClient *internal.HttpClient) MeshPlatformTypeClient { +func newPlatformTypeClient(ctx context.Context, httpClient internal.HttpClient) MeshPlatformTypeClient { return meshPlatformTypeClient{internal.NewMeshObjectClient[MeshPlatformType](ctx, httpClient, "v1")} } diff --git a/project.go b/project.go index 51e7477..2d4f60d 100644 --- a/project.go +++ b/project.go @@ -47,7 +47,7 @@ type meshProjectClient struct { meshObject internal.MeshObjectClient[MeshProject] } -func newProjectClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectClient { +func newProjectClient(ctx context.Context, httpClient internal.HttpClient) MeshProjectClient { return meshProjectClient{internal.NewMeshObjectClient[MeshProject](ctx, httpClient, "v2")} } diff --git a/project_group_binding.go b/project_group_binding.go index a19f970..90eda95 100644 --- a/project_group_binding.go +++ b/project_group_binding.go @@ -20,7 +20,7 @@ type meshProjectGroupBindingClient struct { meshObject internal.MeshObjectClient[MeshProjectGroupBinding] } -func newProjectGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectGroupBindingClient { +func newProjectGroupBindingClient(ctx context.Context, httpClient internal.HttpClient) MeshProjectGroupBindingClient { return meshProjectGroupBindingClient{internal.NewMeshObjectClient[MeshProjectGroupBinding](ctx, httpClient, "v3", "meshprojectbindings", "groupbindings")} } diff --git a/project_user_binding.go b/project_user_binding.go index 75c828f..d6ed6ca 100644 --- a/project_user_binding.go +++ b/project_user_binding.go @@ -20,7 +20,7 @@ type meshProjectUserBindingClient struct { meshObject internal.MeshObjectClient[MeshProjectUserBinding] } -func newProjectUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshProjectUserBindingClient { +func newProjectUserBindingClient(ctx context.Context, httpClient internal.HttpClient) MeshProjectUserBindingClient { return meshProjectUserBindingClient{internal.NewMeshObjectClient[MeshProjectUserBinding](ctx, httpClient, "v3", "meshprojectbindings", "userbindings")} } diff --git a/service_instance.go b/service_instance.go index 2cd537b..50c7dbb 100644 --- a/service_instance.go +++ b/service_instance.go @@ -44,7 +44,7 @@ type MeshServiceInstanceFilter struct { PlanIdentifier *string } -func newServiceInstanceClient(ctx context.Context, httpClient *internal.HttpClient) MeshServiceInstanceClient { +func newServiceInstanceClient(ctx context.Context, httpClient internal.HttpClient) MeshServiceInstanceClient { return meshServiceInstanceClient{internal.NewMeshObjectClient[MeshServiceInstance](ctx, httpClient, "v2")} } diff --git a/tag_definition.go b/tag_definition.go index 61b11bd..2844d0c 100644 --- a/tag_definition.go +++ b/tag_definition.go @@ -79,7 +79,7 @@ type meshTagDefinitionClient struct { meshObject internal.MeshObjectClient[MeshTagDefinition] } -func newTagDefinitionClient(ctx context.Context, httpClient *internal.HttpClient) MeshTagDefinitionClient { +func newTagDefinitionClient(ctx context.Context, httpClient internal.HttpClient) MeshTagDefinitionClient { return meshTagDefinitionClient{internal.NewMeshObjectClient[MeshTagDefinition](ctx, httpClient, "v1")} } diff --git a/tenant.go b/tenant.go index d7929d3..3aa4937 100644 --- a/tenant.go +++ b/tenant.go @@ -57,7 +57,7 @@ type meshTenantClient struct { meshObject internal.MeshObjectClient[MeshTenant] } -func newTenantClient(ctx context.Context, httpClient *internal.HttpClient) MeshTenantClient { +func newTenantClient(ctx context.Context, httpClient internal.HttpClient) MeshTenantClient { return meshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3")} } diff --git a/tenant_v4.go b/tenant_v4.go index 8625136..1968a10 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -74,7 +74,7 @@ type meshTenantV4Client struct { meshObject internal.MeshObjectClient[MeshTenantV4] } -func newTenantV4Client(ctx context.Context, httpClient *internal.HttpClient) MeshTenantV4Client { +func newTenantV4Client(ctx context.Context, httpClient internal.HttpClient) MeshTenantV4Client { return meshTenantV4Client{internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview")} } diff --git a/workspace.go b/workspace.go index 1f47fa6..1950b50 100644 --- a/workspace.go +++ b/workspace.go @@ -43,7 +43,7 @@ type meshWorkspaceClient struct { meshObject internal.MeshObjectClient[MeshWorkspace] } -func newWorkspaceClient(ctx context.Context, httpClient *internal.HttpClient) meshWorkspaceClient { +func newWorkspaceClient(ctx context.Context, httpClient internal.HttpClient) meshWorkspaceClient { return meshWorkspaceClient{internal.NewMeshObjectClient[MeshWorkspace](ctx, httpClient, "v2")} } diff --git a/workspace_group_binding.go b/workspace_group_binding.go index 027afb0..1ff9739 100644 --- a/workspace_group_binding.go +++ b/workspace_group_binding.go @@ -20,7 +20,7 @@ type meshWorkspaceGroupBindingClient struct { meshObject internal.MeshObjectClient[MeshWorkspaceGroupBinding] } -func newWorkspaceGroupBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceGroupBindingClient { +func newWorkspaceGroupBindingClient(ctx context.Context, httpClient internal.HttpClient) MeshWorkspaceGroupBindingClient { return meshWorkspaceGroupBindingClient{internal.NewMeshObjectClient[MeshWorkspaceGroupBinding](ctx, httpClient, "v2", "meshworkspacebindings", "groupbindings")} } diff --git a/workspace_user_binding.go b/workspace_user_binding.go index 501f4ee..13d3ebb 100644 --- a/workspace_user_binding.go +++ b/workspace_user_binding.go @@ -20,7 +20,7 @@ type meshWorkspaceUserBindingClient struct { meshObject internal.MeshObjectClient[MeshWorkspaceUserBinding] } -func newWorkspaceUserBindingClient(ctx context.Context, httpClient *internal.HttpClient) MeshWorkspaceUserBindingClient { +func newWorkspaceUserBindingClient(ctx context.Context, httpClient internal.HttpClient) MeshWorkspaceUserBindingClient { return meshWorkspaceUserBindingClient{internal.NewMeshObjectClient[MeshWorkspaceUserBinding](ctx, httpClient, "v2", "meshworkspacebindings", "userbindings")} } From 02a757c54b2e610c3a7f6ec1fb3065d770e0bc66 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 8 May 2026 23:53:57 +0200 Subject: [PATCH 133/200] feat: retry GET/PUT requests and POST login and add internal.HttpClient unit test --- client.go | 19 ++- internal/auth.go | 18 ++- internal/http_client.go | 13 +- internal/http_client_test.go | 306 +++++++++++++++++++++++++++++++++++ internal/http_error.go | 6 +- internal/logging.go | 12 +- internal/retry.go | 267 ++++++++++++++++++++++++++++++ internal/retry_test.go | 64 ++++++++ 8 files changed, 683 insertions(+), 22 deletions(-) create mode 100644 internal/http_client_test.go create mode 100644 internal/retry.go create mode 100644 internal/retry_test.go diff --git a/client.go b/client.go index aabaccc..3561679 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,6 @@ package client import ( "context" "fmt" - "net/http" "net/url" "time" @@ -47,17 +46,21 @@ func NewApiTokenAuthorization(apiToken string) Authorization { return internal.BearerTokenAuthorization{Token: apiToken} } +const apiLoginPath = "/api/login" + func NewApiKeyAuthorization(apiKey, apiSecret string) Authorization { - return internal.NewClientSecretAuthorization("api/login", apiKey, apiSecret) + return internal.NewClientSecretAuthorization(apiLoginPath, apiKey, apiSecret) } func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authorization) (Client, error) { - httpClient := internal.HttpClient{ - Client: &http.Client{Timeout: 5 * time.Minute}, - RootUrl: rootUrl, - UserAgent: userAgent, - Authorization: auth, - } + httpClient := internal.WithRetry( + internal.NewHttpClient(rootUrl, userAgent, auth), + internal.RetryOptions{ + MaxRetries: 10, + Backoff: internal.ExponentialBackoff{MinWait: 1 * time.Second, MaxWait: 10 * time.Second}, + WhitelistedPaths: map[string][]string{"POST": {apiLoginPath}}, + }, + ) // Check meshStack version compatibility if meshInfo, err := httpClient.GetMeshInfo(ctx); err != nil { diff --git a/internal/auth.go b/internal/auth.go index 4e0db61..dccc658 100644 --- a/internal/auth.go +++ b/internal/auth.go @@ -3,6 +3,8 @@ package internal import ( "context" "fmt" + "net/http" + "sync" "time" ) @@ -12,10 +14,9 @@ type Authorization interface { func NewClientSecretAuthorization(loginApiPath, clientId, clientSecret string) Authorization { return &clientSecretAuthorization{ - BearerTokenAuthorization{}, // empty token initially, is refreshed on demand in ensureValidToken - loginApiPath, - clientId, clientSecret, - time.Time{}, // expiry also set in ensureValidToken + LoginApiPath: loginApiPath, + ClientId: clientId, + ClientSecret: clientSecret, } } @@ -33,9 +34,12 @@ type clientSecretAuthorization struct { ClientId string ClientSecret string ExpiresAt time.Time + mu sync.Mutex } func (auth *clientSecretAuthorization) Header(ctx context.Context, client HttpClient) (string, error) { + auth.mu.Lock() + defer auth.mu.Unlock() if err := auth.ensureValidToken(ctx, client); err != nil { return "", err } @@ -43,7 +47,8 @@ func (auth *clientSecretAuthorization) Header(ctx context.Context, client HttpCl } func (auth *clientSecretAuthorization) ensureValidToken(ctx context.Context, client HttpClient) error { - if auth.Token != "" && time.Until(auth.ExpiresAt) > 30*time.Second { + const minimumTokenLifetime = 30 * time.Second + if auth.Token != "" && time.Until(auth.ExpiresAt) > minimumTokenLifetime { return nil } @@ -59,7 +64,7 @@ func (auth *clientSecretAuthorization) ensureValidToken(ctx context.Context, cli ExpireSec int `json:"expires_in"` } - loginResult, err := unmarshalBody[loginResponse](client.doRequest(ctx, "POST", loginApiUrl, + loginResult, err := unmarshalBody[loginResponse](client.doRequest(ctx, http.MethodPost, loginApiUrl, withPayload(loginRequest{ClientId: auth.ClientId, ClientSecret: auth.ClientSecret}, "application/json")), ) if err != nil { @@ -67,5 +72,6 @@ func (auth *clientSecretAuthorization) ensureValidToken(ctx context.Context, cli } auth.Token = loginResult.Token auth.ExpiresAt = time.Now().Add(time.Duration(loginResult.ExpireSec) * time.Second) + Log.Debug(ctx, "login successful", "url", loginApiUrl, "clientId", auth.ClientId, "expiresAt", auth.ExpiresAt) return nil } diff --git a/internal/http_client.go b/internal/http_client.go index d6c22b5..7b8c149 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -9,10 +9,16 @@ import ( "net/http" "net/url" "slices" + "time" "github.com/meshcloud/terraform-provider-meshstack/client/version" ) +// NewHttpClient creates a new client with an underlying http.Client being a pointer to be modified by WithRetry. +func NewHttpClient(rootUrl *url.URL, userAgent string, auth Authorization) HttpClient { + return HttpClient{&http.Client{Timeout: 5 * time.Minute}, rootUrl, userAgent, auth} +} + // HttpClient wraps [http.Client] with convenient request handling thanks to RequestOption. type HttpClient struct { *http.Client @@ -44,6 +50,9 @@ func (c HttpClient) doRequest(ctx context.Context, method string, url *url.URL, } func (c HttpClient) doAuthorizedRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { + if c.Authorization == nil { + return nil, fmt.Errorf("authorization is not configured") + } authHeader, err := c.Authorization.Header(ctx, c) if err != nil { return nil, err @@ -63,8 +72,8 @@ func (c HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Respo } return responseBody, HttpError{ - StatusCode: res.StatusCode, - Message: string(responseBody), + StatusCode: res.StatusCode, + ResponseBody: responseBody, } } diff --git a/internal/http_client_test.go b/internal/http_client_test.go new file mode 100644 index 0000000..b14d625 --- /dev/null +++ b/internal/http_client_test.go @@ -0,0 +1,306 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/terraform-provider-meshstack/client/version" +) + +func TestHttpClient(t *testing.T) { + t.Run("GetMeshInfo success", func(t *testing.T) { + testLogger := installTestLogger(t) + client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusOK) + _, _ = resp.Write([]byte(`{"version": "2026.10.0"}`)) + + assert.Equal(t, "/mesh/info", req.URL.Path) + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, "test-agent", req.Header.Get("User-Agent")) + }) + info, err := client.GetMeshInfo(t.Context()) + require.NoError(t, err) + assert.Equal(t, &MeshInfo{Version: version.Version{Major: 2026, Minor: 10}}, info) + assert.Equal(t, []string{ + fmt.Sprintf("request [url %s/mesh/info method GET headers User-Agent=test-agent body ]", client.RootUrl), + "response [status 200 body {\n \"version\": \"2026.10.0\"\n}]", + }, testLogger.Debugs) + assert.Empty(t, testLogger.Warns) + }) + + t.Run("GetMeshInfo with successful retry", func(t *testing.T) { + for _, retryableStatusCode := range []int{429, 502, 503, 504} { + t.Run(fmt.Sprintf("after code %d", retryableStatusCode), func(t *testing.T) { + nowUTC := mockTimeNowAsUTC(t) + + testLogger := installTestLogger(t) + retryTestBackoff := retryTestBackoff{WaitTime: 1 * time.Second} + retried := false + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + if !retried { + if retryableStatusCode == 429 { + resp.Header().Set("Retry-After", nowUTC.Add(1*time.Second).Format(http.TimeFormat)) + } + resp.WriteHeader(retryableStatusCode) + retried = true + return + } + resp.WriteHeader(http.StatusOK) + _, _ = resp.Write([]byte(`{}`)) + }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff}) + + _, err := client.GetMeshInfo(t.Context()) + require.NoError(t, err) + if retryableStatusCode == 429 { + assert.Equal(t, 0, retryTestBackoff.Called) + } else { + assert.Equal(t, 1, retryTestBackoff.Called) + } + assert.Equal(t, []string{ + fmt.Sprintf("retrying request [status %d method GET path /mesh/info attempt 1/3 waitTime 1s]", retryableStatusCode), + }, testLogger.Warns) + }) + } + }) + + t.Run("GetMeshInfo with 2 retries exhausted", func(t *testing.T) { + testLogger := installTestLogger(t) + retryTestBackoff := retryTestBackoff{} + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(502) + }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff}) + _, err := client.GetMeshInfo(t.Context()) + var httpErr HttpError + require.ErrorAs(t, err, &httpErr) + assert.Equal(t, 502, httpErr.StatusCode) + assert.Equal(t, 2, retryTestBackoff.Called) + assert.Equal(t, []string{ + "retrying request [status 502 method GET path /mesh/info attempt 1/2 waitTime 0s]", + "retrying request [status 502 method GET path /mesh/info attempt 2/2 waitTime 0s]", + }, testLogger.Warns) + assert.Equal(t, []string{ + fmt.Sprintf("request [url %s/mesh/info method GET headers User-Agent=test-agent body ]", client.RootUrl), + "response [status 502 body ]", + }, testLogger.Debugs) + + }) + + t.Run("GetMeshInfo with context cancelled during backoff", func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(502) + cancel() // cancel context so the backoff wait is interrupted + }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff{WaitTime: 10 * time.Second}}) + _, err := client.GetMeshInfo(ctx) + require.ErrorIs(t, err, context.Canceled) + }) + + t.Run("doRequest with PATCH (not retried)", func(t *testing.T) { + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(200) + }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff{WaitTime: 10 * time.Second}}) + _, err := client.doRequest(t.Context(), http.MethodPatch, client.RootUrl) + require.NoError(t, err) + }) + + t.Run("doRequest with PUT replays body on retry", func(t *testing.T) { + attempt := 0 + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + body, _ := io.ReadAll(req.Body) + assert.JSONEq(t, `{"key":"value"}`, string(body)) + attempt++ + if attempt == 1 { + resp.WriteHeader(502) + return + } + resp.WriteHeader(200) + }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff{}}) + _, err := client.doRequest(t.Context(), http.MethodPut, client.RootUrl, withPayload(map[string]string{"key": "value"}, "application/json")) + require.NoError(t, err) + assert.Equal(t, 2, attempt) + }) + + t.Run("doAuthorizedRequest with BearerTokenAuthorization", func(t *testing.T) { + client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + assert.Equal(t, "Bearer my-static-token", req.Header.Get("Authorization")) + resp.WriteHeader(http.StatusAccepted) + }) + client.Authorization = BearerTokenAuthorization{Token: "my-static-token"} + _, err := client.doAuthorizedRequest(t.Context(), http.MethodPost, client.RootUrl.JoinPath("create"), withPayload("content", "text/plain")) + require.NoError(t, err) + }) + + t.Run("doAuthorizedRequest with clientSecretAuthorization and retries", func(t *testing.T) { + t.Run("succeeds after second attempt", func(t *testing.T) { + retryTestBackoff := retryTestBackoff{} + requestsSeen := map[string]int{} // key is request path + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + defer func() { + requestsSeen[req.URL.Path]++ + }() + if requestsSeen[req.URL.Path] == 0 { + resp.WriteHeader(502) + return + } + switch req.URL.Path { + case "/login": + resp.WriteHeader(http.StatusOK) + // expires_in must be less than minimumTokenLifetime to trigger relogin on second doAuthorizedRequest call + _, _ = resp.Write([]byte(`{"access_token":"some-token", "expires_in": 10}`)) + case "/edit": + assert.Equal(t, "Bearer some-token", req.Header.Get("Authorization")) + resp.WriteHeader(http.StatusAccepted) + default: + t.Fatal("unexpected request", req.URL.Path) + } + }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff, WhitelistedPaths: map[string][]string{http.MethodPost: {"/login"}}}) + client.Authorization = NewClientSecretAuthorization("login", "test-client", "test-client-secret") + resp, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, map[string]int{ + "/login": 2, + "/edit": 2, + }, requestsSeen) + + t.Run("expired token is refreshed with relogin", func(t *testing.T) { + _, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + require.NoError(t, err) + assert.Equal(t, 2, retryTestBackoff.Called) + assert.Equal(t, map[string]int{ + "/login": 3, + "/edit": 3, + }, requestsSeen) + }) + + // two different paths with one retry each, so backoff called twice in total + assert.Equal(t, 2, retryTestBackoff.Called) + }) + + t.Run("succeeds after redirect and retries", func(t *testing.T) { + retryTestBackoff := retryTestBackoff{} + requestsSeen := map[string]int{} + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + defer func() { + requestsSeen[req.URL.Path]++ + }() + if requestsSeen[req.URL.Path] == 0 { + resp.WriteHeader(502) + return + } + switch req.URL.Path { + case "/login": + body, _ := io.ReadAll(req.Body) + assert.JSONEq(t, `{"clientId":"test-client","clientSecret":"test-client-secret"}`, string(body)) + http.Redirect(resp, req, "/login-target", http.StatusTemporaryRedirect) + case "/login-target": + body, _ := io.ReadAll(req.Body) + assert.JSONEq(t, `{"clientId":"test-client","clientSecret":"test-client-secret"}`, string(body)) + resp.WriteHeader(http.StatusOK) + _, _ = resp.Write([]byte(`{"access_token":"redirected-token", "expires_in": 3600}`)) + case "/edit": + assert.Equal(t, "Bearer redirected-token", req.Header.Get("Authorization")) + resp.WriteHeader(http.StatusAccepted) + default: + t.Fatal("unexpected request", req.URL.Path) + } + }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff, WhitelistedPaths: map[string][]string{http.MethodPost: {"/login"}}}) + client.Authorization = NewClientSecretAuthorization("login", "test-client", "test-client-secret") + _, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + require.NoError(t, err) + assert.Equal(t, map[string]int{ + "/login": 2, // 1st: 502, 2nd: 307 redirect + "/login-target": 2, // 1st: 502, 2nd: 200 + "/edit": 2, // 1st: 502, 2nd: 202 + }, requestsSeen) + assert.Equal(t, 3, retryTestBackoff.Called) // one retry each for /login, /login-target, /edit + }) + + t.Run("fails constantly at login", func(t *testing.T) { + retryTestBackoff := retryTestBackoff{} + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, r *http.Request) { + resp.WriteHeader(503) + }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff, WhitelistedPaths: map[string][]string{http.MethodPost: {"/login"}}}) + client.Authorization = NewClientSecretAuthorization("login", "test-client", "test-client-secret") + _, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + require.ErrorContains(t, err, fmt.Sprintf("login at %s/login with client id 'test-client' failed", client.RootUrl)) + var httpErr HttpError + require.ErrorAs(t, err, &httpErr) + assert.Equal(t, 503, httpErr.StatusCode) + assert.Equal(t, 2, retryTestBackoff.Called) + }) + + }) +} + +func mockTimeNowAsUTC(t *testing.T) time.Time { + t.Helper() + now := time.Now().UTC().Truncate(time.Second) + timeNow = func() time.Time { return now } + t.Cleanup(func() { + timeNow = time.Now + }) + return now +} + +func newTestClientWithServer(t *testing.T, handlerFunc http.HandlerFunc) HttpClient { + t.Helper() + server := httptest.NewServer(handlerFunc) + t.Cleanup(server.Close) + rootUrl, err := url.Parse(server.URL) + require.NoError(t, err) + client := server.Client() + return HttpClient{ + Client: client, + RootUrl: rootUrl, + UserAgent: "test-agent", + } +} + +func installTestLogger(t *testing.T) *testLogger { + t.Helper() + testLogger := &testLogger{} + previousLog := Log + Log = testLogger + t.Cleanup(func() { + Log = previousLog + }) + return testLogger +} + +type testLogger struct { + Debugs []string + Infos []string + Warns []string +} + +func (c *testLogger) Debug(_ context.Context, msg string, args ...any) { + c.Debugs = append(c.Debugs, fmt.Sprintf("%s %v", msg, args)) +} + +func (c *testLogger) Info(_ context.Context, msg string, args ...any) { + c.Infos = append(c.Infos, fmt.Sprintf("%s %v", msg, args)) +} + +func (c *testLogger) Warn(_ context.Context, msg string, args ...any) { + c.Warns = append(c.Warns, fmt.Sprintf("%s %v", msg, args)) +} + +type retryTestBackoff struct { + WaitTime time.Duration + Called int +} + +func (b *retryTestBackoff) Calculate(int) time.Duration { + b.Called++ + return b.WaitTime +} diff --git a/internal/http_error.go b/internal/http_error.go index 7b5d3e8..92030fc 100644 --- a/internal/http_error.go +++ b/internal/http_error.go @@ -8,12 +8,12 @@ import ( // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. type HttpError struct { - StatusCode int - Message string + StatusCode int + ResponseBody []byte } func (e HttpError) Error() string { - return fmt.Sprintf("http error %d: %s", e.StatusCode, e.Message) + return fmt.Sprintf("http error %d, response '%s'", e.StatusCode, string(e.ResponseBody)) } // IsForbidden returns true if the error is a 403 Forbidden response. diff --git a/internal/logging.go b/internal/logging.go index b45315e..d82cda8 100644 --- a/internal/logging.go +++ b/internal/logging.go @@ -14,19 +14,25 @@ import ( var Log Logger = noopLogger{} -// Logger only supports Debug and Info log levels. +// Logger supports Debug, Info, and Warn log levels. +// Note that msg is a short, descriptive statement what is logged, and args are key value pairs (values are string or implement fmt.Stringer). type Logger interface { - Info(ctx context.Context, msg string, args ...any) Debug(ctx context.Context, msg string, args ...any) + Info(ctx context.Context, msg string, args ...any) + Warn(ctx context.Context, msg string, args ...any) } type noopLogger struct{} +func (n noopLogger) Debug(context.Context, string, ...any) { + // do nothing +} + func (n noopLogger) Info(context.Context, string, ...any) { // do nothing } -func (n noopLogger) Debug(context.Context, string, ...any) { +func (n noopLogger) Warn(context.Context, string, ...any) { // do nothing } diff --git a/internal/retry.go b/internal/retry.go new file mode 100644 index 0000000..6906422 --- /dev/null +++ b/internal/retry.go @@ -0,0 +1,267 @@ +package internal + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "math" + "net/http" + "strconv" + "sync" + "time" +) + +// WithRetry sets up the given client to retry certain requests. +// GET and PUT are retried by default, POST only if the path is explicitly whitelisted. +// See RetryOptions. +func WithRetry(c HttpClient, options RetryOptions) HttpClient { + next := http.DefaultTransport + if c.Transport != nil { + next = c.Transport + } + whitelistedByMethodAndUrl := func() (m map[string]*sync.Map) { + m = make(map[string]*sync.Map) + for method, paths := range options.WhitelistedPaths { + m[method] = new(sync.Map) + for _, path := range paths { + m[method].Store(c.RootUrl.JoinPath(path).String(), nil) + } + } + return + }() + c.Transport = &retryRoundTripper{ + Next: next, + MaxRetries: options.MaxRetries, + // ShouldRetryRequest checks if the request method/path is eligible for retry. + ShouldRetryRequest: func(req *http.Request) (retry bool) { + if options.Backoff == nil { + return false + } + switch req.Method { + case http.MethodGet, http.MethodPut: + return true + } + if whitelisted, found := whitelistedByMethodAndUrl[req.Method]; found { + _, retry = whitelisted.Load(req.URL.String()) + } + return + }, + // ShouldRetryResponse returns the backoff policy if the response/error indicates a retryable condition, + // otherwise nil is returned to indicate no retry. + ShouldRetryResponse: func(resp *http.Response, err error) RetryBackoff { + if err != nil { + return options.Backoff + } + switch resp.StatusCode { + case http.StatusTooManyRequests, http.StatusServiceUnavailable: + return retryAfterBackoff{Response: resp, Fallback: options.Backoff} + case http.StatusBadGateway, http.StatusGatewayTimeout: + return options.Backoff + case http.StatusTemporaryRedirect, http.StatusPermanentRedirect: + if locationRedirectUrl, _ := resp.Request.URL.Parse(resp.Header.Get("Location")); locationRedirectUrl != nil { + if whitelisted, found := whitelistedByMethodAndUrl[resp.Request.Method]; found { + whitelisted.Store(locationRedirectUrl.String(), nil) + } + } + return nil + default: + return nil + } + }, + } + return c // for fluent API +} + +// RetryOptions configure WithRetry. +type RetryOptions struct { + // MaxRetries limits the attempts to retries. If zero, retries will never be attempted. + MaxRetries int + // Backoff to use when retrying. If nil, retries will never be attempted. + Backoff RetryBackoff + // WhitelistedPaths allow methods beyond GET and PUT to be retried as well, see WithRetry. + WhitelistedPaths map[string][]string +} + +// RetryBackoff calculates the duration to wait before the next retry attempt. +type RetryBackoff interface { + Calculate(attempt int) time.Duration +} + +// ExponentialBackoff increases the backoff exponentially: minWait * 2^(attempt-1). +type ExponentialBackoff struct { + MinWait, MaxWait time.Duration +} + +func (b ExponentialBackoff) Calculate(attempt int) time.Duration { + nextWait := time.Duration(math.Pow(2, float64(attempt-1))) * b.MinWait + if b.MaxWait > 0 && nextWait > b.MaxWait { + return b.MaxWait + } + return nextWait +} + +var timeNow = time.Now + +type retryAfterBackoff struct { + Response *http.Response + Fallback RetryBackoff +} + +func (b retryAfterBackoff) Calculate(attempt int) (waitTime time.Duration) { + defer func() { + const maxRetryAfterWaitTime = 5 * time.Minute + if waitTime < 0 { + waitTime = b.Fallback.Calculate(attempt) + } else if waitTime > maxRetryAfterWaitTime { + waitTime = maxRetryAfterWaitTime + } + }() + + // Parse the Retry-After header from a response. + // It supports both delay-seconds and HTTP-date formats (RFC 7231 §7.1.3). + + header := b.Response.Header.Get("Retry-After") + if header == "" { + return -1 + } + + // Try as delay-seconds first. + if seconds, err := strconv.ParseInt(header, 10, 64); err == nil { + return time.Duration(seconds) * time.Second + } + + // Try as HTTP-date (RFC 7231). + if date, err := http.ParseTime(header); err == nil { + return date.Sub(timeNow()) + } + return -1 +} + +// retryRoundTripper wraps an http.RoundTripper to retry failed requests. +// See WithRetry for which methods are retried. +type retryRoundTripper struct { + Next http.RoundTripper + MaxRetries int + ShouldRetryRequest func(req *http.Request) bool + ShouldRetryResponse func(resp *http.Response, err error) RetryBackoff +} + +func (r *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if !r.ShouldRetryRequest(req) { + return r.Next.RoundTrip(req) + } + req = makeRequestBodyRetryable(req) + for attempt := 1; ; attempt++ { + resp, err := r.Next.RoundTrip(req) + if errors.Is(err, errRetryableBodyClose) { + return resp, err + } + backoff := r.ShouldRetryResponse(resp, err) + // No retry needed or no more retries left — return as-is. + if backoff == nil || attempt > r.MaxRetries { + return resp, err + } + drainAndCloseResponseBody(req.Context(), resp) + if req.GetBody != nil { + if body, err := req.GetBody(); err != nil { + return nil, err + } else { + req.Body = body + } + } + waitTime := backoff.Calculate(attempt) + Log.Warn(req.Context(), "retrying request", append( + func() []any { + if err != nil { + return []any{"error", err.Error()} + } + return []any{"status", resp.StatusCode} + }(), + "method", req.Method, + "path", req.URL.Path, + "attempt", fmt.Sprintf("%d/%d", attempt, r.MaxRetries), + "waitTime", waitTime, + )...) + timer := time.NewTimer(waitTime) + select { + case <-req.Context().Done(): + timer.Stop() + return nil, req.Context().Err() + case <-timer.C: + } + } +} + +func makeRequestBodyRetryable(req *http.Request) *http.Request { + if req.Body == nil { + return req + } + // If GetBody already returns independent readers (e.g. set by http.NewRequestWithContext + // for *bytes.Buffer, *bytes.Reader, *strings.Reader), use it as-is for retries. + if req.GetBody != nil { + return req + } + body := retryableBody{Closer: req.Body} + body.Reader = io.TeeReader(req.Body, &body.Buffer) + result := req.Clone(req.Context()) + result.Body = &body + result.GetBody = nil + return result +} + +// retryableBody lazily captures request body bytes on the first read and replays them on retries. +// Buffer is filled via TeeReader as the transport reads during the first request. On Close, the +// source is released and subsequent reads replay from Buffer via bytes.NewReader. +type retryableBody struct { + io.Reader + io.Closer + Buffer appendWriter +} + +var errRetryableBodyClose = errors.New("retryableBody failed to close") + +func (b *retryableBody) Close() error { + // Drain remaining bytes through the TeeReader to ensure Buffer captures the full body, + // even if the transport only partially read it (e.g. connection reset mid-write). + if _, err := io.Copy(io.Discard, b.Reader); err != nil { + return errors.Join(err, errRetryableBodyClose) + } + // On first close, close the Body and use the b.Buffer from now on + if b.Closer != nil { + if err := b.Closer.Close(); err != nil { + return errors.Join(err, errRetryableBodyClose) + } + } + b.Closer = nil + b.Reader = bytes.NewReader(b.Buffer) + return nil +} + +// appendWriter is an io.Writer that appends to a []byte slice. +// Helper for retryableBody.Buffer. +type appendWriter []byte + +func (w *appendWriter) Write(p []byte) (int, error) { + *w = append(*w, p...) + return len(p), nil +} + +// drainAndCloseResponseBody reads up to maxBytes from the response body before closing it. +// Draining enables Go's http.Transport to reuse the underlying TCP connection for +// subsequent requests. The maxBytes limit prevents getting stuck on large or slow +// responses — if the body exceeds this limit, the connection won't be reused, but +// we won't block indefinitely either. +func drainAndCloseResponseBody(ctx context.Context, resp *http.Response) { + const maxBytes = 16 * 1024 + if resp != nil && resp.Body != nil { + drainedBytes, err := io.CopyN(io.Discard, resp.Body, maxBytes) + if err != nil && !errors.Is(err, io.EOF) { + Log.Debug(ctx, fmt.Sprintf("failed to drain response body: %s", err.Error())) + } + if err := resp.Body.Close(); err != nil { + Log.Debug(ctx, fmt.Sprintf("failed to close response body after draining %d bytes: %s", drainedBytes, err.Error())) + } + } +} diff --git a/internal/retry_test.go b/internal/retry_test.go new file mode 100644 index 0000000..a643edd --- /dev/null +++ b/internal/retry_test.go @@ -0,0 +1,64 @@ +package internal + +import ( + "fmt" + "net/http" + "testing" + "testing/synctest" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestExponentialBackoff_Calculate(t *testing.T) { + tests := []struct { + attempt int + want time.Duration + }{ + {1, 1 * time.Second}, + {2, 2 * time.Second}, + {3, 4 * time.Second}, + {4, 5 * time.Second}, + {5, 5 * time.Second}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("attempt %d", tt.attempt), func(t *testing.T) { + b := ExponentialBackoff{ + MinWait: 1 * time.Second, + MaxWait: 5 * time.Second, + } + assert.Equalf(t, tt.want, b.Calculate(tt.attempt), "Calculate(%v)", tt.attempt) + }) + } +} + +func TestRetryAfterBackoff(t *testing.T) { + // synctest bubble starts at 2000-01-01T00:00:00Z + bubbleStart := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + fallback := ExponentialBackoff{MinWait: 1 * time.Second, MaxWait: 10 * time.Second} + + tests := []struct { + name string + header string + want time.Duration + }{ + {"delay-seconds", "30", 30 * time.Second}, + {"zero seconds", "0", 0}, // RFC: retry immediately + {"capped at 5 minutes", "600", 5 * time.Minute}, // capped + {"empty header", "", 1 * time.Second}, // falls back + {"unparseable header", "not-a-number-or-date", 1 * time.Second}, // falls back + {"HTTP-date in the past", bubbleStart.Add(-10 * time.Second).Format(http.TimeFormat), 1 * time.Second}, // falls back + {"HTTP-date in the future", bubbleStart.Add(45 * time.Second).Format(http.TimeFormat), 45 * time.Second}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + b := retryAfterBackoff{ + Response: &http.Response{Header: http.Header{"Retry-After": {tt.header}}}, + Fallback: fallback, + } + assert.Equal(t, tt.want, b.Calculate(1)) + }) + }) + } +} From 91c85f4237e2163221e7edea8b14c05e3c579133 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Wed, 6 May 2026 09:24:57 +0200 Subject: [PATCH 134/200] feat: adapt building_block_v2 to moved createdOn field in upstream API The meshStack API has moved the building block creation timestamp from metadata.createdOn to status.lifecycle.createdOn. This change updates the Terraform provider to reflect the new API structure by: Note: Existing Terraform state will need a refresh after upgrading the provider to migrate from the old path to the new one. --- buildingblock_v2.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 66ba7ae..42a7347 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -24,11 +24,8 @@ type MeshBuildingBlockV2 struct { } type MeshBuildingBlockV2Metadata struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - CreatedOn string `json:"createdOn" tfsdk:"created_on"` - MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` - MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` + Uuid string `json:"uuid" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } type MeshBuildingBlockV2Spec struct { @@ -54,10 +51,18 @@ type MeshBuildingBlockV2Create struct { Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` } +type MeshBuildingBlockV2Lifecycle struct { + State string `json:"state" tfsdk:"state"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` + MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` +} + type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` } type MeshBuildingBlockV2Client interface { From 135a43b0f05f45788e659baca2d061386e28553c Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 7 May 2026 17:21:03 +0200 Subject: [PATCH 135/200] fix: adapt building block definition after changes in upstream API --- buildingblock_definition.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/buildingblock_definition.go b/buildingblock_definition.go index 667f5fb..5259cc1 100644 --- a/buildingblock_definition.go +++ b/buildingblock_definition.go @@ -22,7 +22,10 @@ type MeshBuildingBlockDefinitionMetadata struct { Tags map[string][]string `json:"tags" tfsdk:"tags"` } -type BuildingBlockDefinitionSupportedPlatform string +type BuildingBlockDefinitionSupportedPlatform struct { + Kind string `json:"kind" tfsdk:"kind"` + Name string `json:"name" tfsdk:"name"` +} type MeshBuildingBlockDefinitionSpec struct { DisplayName string `json:"displayName" tfsdk:"display_name"` @@ -34,11 +37,9 @@ type MeshBuildingBlockDefinitionSpec struct { SupportURL *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationURL *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` // NotificationSubscribers can also specify emails with prefix 'email:', so it's not only usernames (as the JSON field name suggests)! - NotificationSubscribers types.Set[string] `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` - Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` - // SupportedPlatforms are currently platform types only. Specifying single platforms is currently unsupported. - // Have this list of string with a dedicated type, to convert it to/from Platform Type refs. - SupportedPlatforms types.Set[BuildingBlockDefinitionSupportedPlatform] `json:"supportedPlatforms" tfsdk:"supported_platforms"` + NotificationSubscribers types.Set[string] `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` + Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` + SupportedPlatforms types.Set[BuildingBlockDefinitionSupportedPlatform] `json:"supportedPlatforms" tfsdk:"supported_platforms"` } type MeshBuildingBlockDefinitionStatusVersion struct { From 686a3bcda0f9ba0ab902fc121af8e14e8f4ec222 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 12 May 2026 11:31:02 +0200 Subject: [PATCH 136/200] fix: do not expose lifecycle in BB schema (resource/datasource) see 6b5b41dce00a9c4240b59c04d73779141e00e1cc --- buildingblock_v2.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 42a7347..d80d48a 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -51,18 +51,10 @@ type MeshBuildingBlockV2Create struct { Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` } -type MeshBuildingBlockV2Lifecycle struct { - State string `json:"state" tfsdk:"state"` - CreatedOn string `json:"createdOn" tfsdk:"created_on"` - MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` - MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` -} - type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` - Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` } type MeshBuildingBlockV2Client interface { From c9fcd8905872796af6896a4cf16fa9d4f206e77f Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 18 May 2026 15:39:49 +0200 Subject: [PATCH 137/200] fix: add lifecycle state tracking to building_block_v2 - Add MeshBuildingBlockV2Lifecycle struct to capture lifecycle.state from API - Expose status.lifecycle in resource and data source schemas - Update DeletionSuccessful() to recognize DELETED lifecycle state as done - Remove resource from state when lifecycle.state == DELETED - Fix missing force_purge attribute in resource state assignment --- buildingblock_v2.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index d80d48a..6379d62 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -15,6 +15,7 @@ const ( BUILDING_BLOCK_STATUS_IN_PROGRESS = "IN_PROGRESS" BUILDING_BLOCK_STATUS_SUCCEEDED = "SUCCEEDED" BUILDING_BLOCK_STATUS_FAILED = "FAILED" + BUILDING_BLOCK_LIFECYCLE_STATE_DELETED = "DELETED" ) type MeshBuildingBlockV2 struct { @@ -51,10 +52,15 @@ type MeshBuildingBlockV2Create struct { Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` } +type MeshBuildingBlockV2Lifecycle struct { + State string `json:"state" tfsdk:"state"` +} + type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` } type MeshBuildingBlockV2Client interface { @@ -106,6 +112,8 @@ func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) { switch { case bb == nil: done = true + case bb.Status.Lifecycle.State == BUILDING_BLOCK_LIFECYCLE_STATE_DELETED: + done = true case bb.Status.Status == BUILDING_BLOCK_STATUS_FAILED: err = fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", bb.Metadata.Uuid) } From 23b2ddc5095a12ea238d72da09e31a5fdcd45795 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 18 May 2026 16:10:53 +0200 Subject: [PATCH 138/200] chore: document all lifecycle states for building_block_v2 --- buildingblock_v2.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 6379d62..95a5441 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -9,13 +9,15 @@ import ( const ( // Building Block Status Constants. - BUILDING_BLOCK_STATUS_WAITING_FOR_DEPENDENT_INPUT = "WAITING_FOR_DEPENDENT_INPUT" - BUILDING_BLOCK_STATUS_WAITING_FOR_OPERATOR_INPUT = "WAITING_FOR_OPERATOR_INPUT" - BUILDING_BLOCK_STATUS_PENDING = "PENDING" - BUILDING_BLOCK_STATUS_IN_PROGRESS = "IN_PROGRESS" - BUILDING_BLOCK_STATUS_SUCCEEDED = "SUCCEEDED" - BUILDING_BLOCK_STATUS_FAILED = "FAILED" - BUILDING_BLOCK_LIFECYCLE_STATE_DELETED = "DELETED" + BUILDING_BLOCK_STATUS_WAITING_FOR_DEPENDENT_INPUT = "WAITING_FOR_DEPENDENT_INPUT" + BUILDING_BLOCK_STATUS_WAITING_FOR_OPERATOR_INPUT = "WAITING_FOR_OPERATOR_INPUT" + BUILDING_BLOCK_STATUS_PENDING = "PENDING" + BUILDING_BLOCK_STATUS_IN_PROGRESS = "IN_PROGRESS" + BUILDING_BLOCK_STATUS_SUCCEEDED = "SUCCEEDED" + BUILDING_BLOCK_STATUS_FAILED = "FAILED" + BUILDING_BLOCK_LIFECYCLE_STATE_ACTIVE = "ACTIVE" + BUILDING_BLOCK_LIFECYCLE_STATE_MARKED_FOR_DELETION = "MARKED_FOR_DELETION" + BUILDING_BLOCK_LIFECYCLE_STATE_DELETED = "DELETED" ) type MeshBuildingBlockV2 struct { @@ -111,6 +113,8 @@ func (bb *MeshBuildingBlockV2) CreateSuccessful() (done bool, err error) { func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) { switch { case bb == nil: + // Expected when receiving a 404 (hard deletion), default behavior until meshStack v2026.20.0. + // For versions higher than that, we get a building block back with a lifecycle state to inspect. done = true case bb.Status.Lifecycle.State == BUILDING_BLOCK_LIFECYCLE_STATE_DELETED: done = true From dddf8ffaad541b24f616064e014f24b93544240d Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Tue, 19 May 2026 09:56:12 +0200 Subject: [PATCH 139/200] chore: add test for BB deletion Follow-up on https://github.com/meshcloud/terraform-provider-meshstack/pull/172/changes#r3264364813 --- buildingblock_v2_test.go | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 buildingblock_v2_test.go diff --git a/buildingblock_v2_test.go b/buildingblock_v2_test.go new file mode 100644 index 0000000..312887d --- /dev/null +++ b/buildingblock_v2_test.go @@ -0,0 +1,67 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { + tests := []struct { + name string + bb *MeshBuildingBlockV2 + wantDone bool + wantErr bool + }{ + { + name: "nil (hard deletion / 404)", + bb: nil, + wantDone: true, + wantErr: false, + }, + { + name: "lifecycle state DELETED", + bb: &MeshBuildingBlockV2{ + Status: MeshBuildingBlockV2Status{ + Lifecycle: MeshBuildingBlockV2Lifecycle{State: BUILDING_BLOCK_LIFECYCLE_STATE_DELETED}, + }, + }, + wantDone: true, + wantErr: false, + }, + { + name: "status FAILED during deletion", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: "test-uuid"}, + Status: MeshBuildingBlockV2Status{ + Status: BUILDING_BLOCK_STATUS_FAILED, + }, + }, + wantDone: false, + wantErr: true, + }, + { + name: "still in progress (MARKED_FOR_DELETION lifecycle, non-failed status)", + bb: &MeshBuildingBlockV2{ + Status: MeshBuildingBlockV2Status{ + Status: BUILDING_BLOCK_STATUS_IN_PROGRESS, + Lifecycle: MeshBuildingBlockV2Lifecycle{State: BUILDING_BLOCK_LIFECYCLE_STATE_MARKED_FOR_DELETION}, + }, + }, + wantDone: false, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + done, err := tt.bb.DeletionSuccessful() + assert.Equal(t, tt.wantDone, done) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} From dfc7983dc85b90061728bd66015a8240e31e0efb Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 15 May 2026 16:34:30 +0200 Subject: [PATCH 140/200] fix: migrate building block target references CU-86c9uebcz --- buildingblock_v2.go | 6 +++--- client.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 95a5441..7a6b8bc 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -45,9 +45,9 @@ type MeshBuildingBlockV2DefinitionVersionRef struct { } type MeshBuildingBlockV2TargetRef struct { - Kind string `json:"kind" tfsdk:"kind"` - Uuid *string `json:"uuid" tfsdk:"uuid"` - Identifier *string `json:"identifier" tfsdk:"identifier"` + Kind string `json:"kind" tfsdk:"kind"` + Uuid *string `json:"uuid" tfsdk:"uuid"` + Name *string `json:"name" tfsdk:"name"` } type MeshBuildingBlockV2Create struct { diff --git a/client.go b/client.go index 3561679..ed6ff80 100644 --- a/client.go +++ b/client.go @@ -10,7 +10,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.10.0") +var MinMeshStackVersion = version.MustParse("2026.22.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. From abd332c33c896aa233782321bf21638ef5160ef8 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 21 May 2026 17:07:29 +0200 Subject: [PATCH 141/200] feat: introduce MESHSTACK_SKIP_VERSION_CHECK to skip version check --- client.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index ed6ff80..0616f45 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "os" "time" "github.com/meshcloud/terraform-provider-meshstack/client/internal" @@ -66,7 +67,11 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza if meshInfo, err := httpClient.GetMeshInfo(ctx); err != nil { return Client{}, fmt.Errorf("failed to retrieve meshStack version information from /mesh/info endpoint: %w", err) } else if meshInfo.Version.Less(MinMeshStackVersion) { - return Client{}, fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) + skipVersionCheck := os.Getenv("MESHSTACK_SKIP_VERSION_CHECK") == "true" + + if !skipVersionCheck { + return Client{}, fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) + } } return Client{ From 58adb1b9887267321781854c0cef5f51278123c1 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 28 May 2026 09:50:42 +0200 Subject: [PATCH 142/200] fix: change input/output structure from array to map CU-86c9p4kcd --- buildingblock_v2.go | 26 ++++++++++++++++++++------ client.go | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 7a6b8bc..9bef5a5 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -36,8 +36,22 @@ type MeshBuildingBlockV2Spec struct { TargetRef MeshBuildingBlockV2TargetRef `json:"targetRef" tfsdk:"target_ref"` DisplayName string `json:"displayName" tfsdk:"display_name"` - Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` - ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` + Inputs map[string]MeshBuildingBlockV2Input `json:"inputs" tfsdk:"-"` + ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` +} + +type MeshBuildingBlockV2Input struct { + Value any `json:"value"` + ValueType string `json:"valueType"` + IsSensitive bool `json:"isSensitive"` + AssignmentType *string `json:"assignmentType"` + UpdateableByConsumer bool `json:"updateableByConsumer"` +} + +type MeshBuildingBlockV2Output struct { + Value any `json:"value"` + ValueType string `json:"valueType"` + AssignmentType *string `json:"assignmentType"` } type MeshBuildingBlockV2DefinitionVersionRef struct { @@ -59,10 +73,10 @@ type MeshBuildingBlockV2Lifecycle struct { } type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` - Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` + Status string `json:"status" tfsdk:"status"` + Outputs map[string]MeshBuildingBlockV2Output `json:"outputs" tfsdk:"-"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` } type MeshBuildingBlockV2Client interface { diff --git a/client.go b/client.go index 0616f45..f818ca4 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.22.0") +var MinMeshStackVersion = version.MustParse("2026.23.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. From 34926e5ad873e1cd6b4f30c6e6b70a2caa877872 Mon Sep 17 00:00:00 2001 From: Mohammad Alhussan Date: Tue, 26 May 2026 11:40:25 +0200 Subject: [PATCH 143/200] feat: add meshstack_building_block_runner resource --- buildingblock_runner.go | 94 +++++++++++++++++++++++++++++++++++++++++ client.go | 2 + 2 files changed, 96 insertions(+) diff --git a/buildingblock_runner.go b/buildingblock_runner.go index e73e102..e14bbaa 100644 --- a/buildingblock_runner.go +++ b/buildingblock_runner.go @@ -1,6 +1,100 @@ package client +import ( + "context" + "fmt" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + +type MeshBuildingBlockRunnerImplementationType string + +const ( + MeshBuildingBlockRunnerImplementationTypeTerraform MeshBuildingBlockRunnerImplementationType = "TERRAFORM" + MeshBuildingBlockRunnerImplementationTypeGithubWorkflow MeshBuildingBlockRunnerImplementationType = "GITHUB_WORKFLOW" + MeshBuildingBlockRunnerImplementationTypeGitlabPipeline MeshBuildingBlockRunnerImplementationType = "GITLAB_PIPELINE" + MeshBuildingBlockRunnerImplementationTypeAzureDevopsPipeline MeshBuildingBlockRunnerImplementationType = "AZURE_DEVOPS_PIPELINE" + MeshBuildingBlockRunnerImplementationTypeManual MeshBuildingBlockRunnerImplementationType = "MANUAL" +) + +var MeshBuildingBlockRunnerImplementationTypes = []string{ + string(MeshBuildingBlockRunnerImplementationTypeTerraform), + string(MeshBuildingBlockRunnerImplementationTypeGithubWorkflow), + string(MeshBuildingBlockRunnerImplementationTypeGitlabPipeline), + string(MeshBuildingBlockRunnerImplementationTypeAzureDevopsPipeline), + string(MeshBuildingBlockRunnerImplementationTypeManual), +} + type BuildingBlockRunnerRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` Kind string `json:"kind" tfsdk:"kind"` } + +type MeshBuildingBlockRunner struct { + Metadata MeshBuildingBlockRunnerMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockRunnerSpec `json:"spec" tfsdk:"spec"` +} + +type MeshBuildingBlockRunnerMetadata struct { + Uuid *string `json:"uuid,omitempty" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn *string `json:"createdOn,omitempty" tfsdk:"created_on"` + LastSeen *string `json:"lastSeen,omitempty" tfsdk:"last_seen"` +} + +type MeshBuildingBlockRunnerSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + PublicKey string `json:"publicKey" tfsdk:"public_key"` + ImplementationType string `json:"implementationType" tfsdk:"implementation_type"` + Restriction *string `json:"restriction,omitempty" tfsdk:"restriction"` + IsSelfHosted *bool `json:"isSelfHosted,omitempty" tfsdk:"is_self_hosted"` + WorkloadIdentityFederation *MeshRunnerWorkloadIdentityFed `json:"workloadIdentityFederation,omitempty" tfsdk:"workload_identity_federation"` +} + +type MeshRunnerWorkloadIdentityFed struct { + Subject *string `json:"subject,omitempty" tfsdk:"subject"` + Issuer *string `json:"issuer,omitempty" tfsdk:"issuer"` + Gcp *MeshRunnerWifProviderConfig `json:"gcp,omitempty" tfsdk:"gcp"` + Aws *MeshRunnerWifProviderConfig `json:"aws,omitempty" tfsdk:"aws"` + Azure *MeshRunnerWifProviderConfig `json:"azure,omitempty" tfsdk:"azure"` +} + +type MeshRunnerWifProviderConfig struct { + Audience string `json:"audience" tfsdk:"audience"` + TokenPath string `json:"tokenPath" tfsdk:"token_path"` +} + +type MeshBuildingBlockRunnerClient interface { + Create(ctx context.Context, runner MeshBuildingBlockRunner) (*MeshBuildingBlockRunner, error) + Read(ctx context.Context, uuid string) (*MeshBuildingBlockRunner, error) + Update(ctx context.Context, runner MeshBuildingBlockRunner) (*MeshBuildingBlockRunner, error) + Delete(ctx context.Context, uuid string) error +} + +type meshBuildingBlockRunnerClient struct { + meshObject internal.MeshObjectClient[MeshBuildingBlockRunner] +} + +func newBuildingBlockRunnerClient(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockRunnerClient { + return meshBuildingBlockRunnerClient{internal.NewMeshObjectClient[MeshBuildingBlockRunner](ctx, httpClient, "v1-preview")} +} + +func (c meshBuildingBlockRunnerClient) Create(ctx context.Context, runner MeshBuildingBlockRunner) (*MeshBuildingBlockRunner, error) { + return c.meshObject.Post(ctx, runner) +} + +func (c meshBuildingBlockRunnerClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlockRunner, error) { + return c.meshObject.Get(ctx, uuid) +} + +func (c meshBuildingBlockRunnerClient) Update(ctx context.Context, runner MeshBuildingBlockRunner) (*MeshBuildingBlockRunner, error) { + if runner.Metadata.Uuid == nil || *runner.Metadata.Uuid == "" { + return nil, fmt.Errorf("missing metadata.uuid") + } + + return c.meshObject.Put(ctx, *runner.Metadata.Uuid, runner) +} + +func (c meshBuildingBlockRunnerClient) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) +} diff --git a/client.go b/client.go index f818ca4..9a1f366 100644 --- a/client.go +++ b/client.go @@ -23,6 +23,7 @@ type Client struct { BuildingBlockV2 MeshBuildingBlockV2Client BuildingBlockDefinition MeshBuildingBlockDefinitionClient BuildingBlockDefinitionVersion MeshBuildingBlockDefinitionVersionClient + BuildingBlockRunner MeshBuildingBlockRunnerClient Integration MeshIntegrationClient LandingZone MeshLandingZoneClient Location MeshLocationClient @@ -80,6 +81,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza BuildingBlockV2: newBuildingBlockV2Client(ctx, httpClient), BuildingBlockDefinition: newBuildingBlockDefinitionClient(ctx, httpClient), BuildingBlockDefinitionVersion: newBuildingBlockDefinitionVersionClient(ctx, httpClient), + BuildingBlockRunner: newBuildingBlockRunnerClient(ctx, httpClient), Integration: newIntegrationClient(ctx, httpClient), LandingZone: newLandingZoneClient(ctx, httpClient), Location: newLocationClient(ctx, httpClient), From 92bffdac30d08e4f453a550439984f00307bd2b9 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 29 May 2026 15:50:38 +0200 Subject: [PATCH 144/200] fix: revert "change input/output structure from array to map" This reverts commit e0f63173a173e6faada285838a92357402f14076. --- buildingblock_v2.go | 26 ++++++-------------------- client.go | 2 +- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 9bef5a5..7a6b8bc 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -36,22 +36,8 @@ type MeshBuildingBlockV2Spec struct { TargetRef MeshBuildingBlockV2TargetRef `json:"targetRef" tfsdk:"target_ref"` DisplayName string `json:"displayName" tfsdk:"display_name"` - Inputs map[string]MeshBuildingBlockV2Input `json:"inputs" tfsdk:"-"` - ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` -} - -type MeshBuildingBlockV2Input struct { - Value any `json:"value"` - ValueType string `json:"valueType"` - IsSensitive bool `json:"isSensitive"` - AssignmentType *string `json:"assignmentType"` - UpdateableByConsumer bool `json:"updateableByConsumer"` -} - -type MeshBuildingBlockV2Output struct { - Value any `json:"value"` - ValueType string `json:"valueType"` - AssignmentType *string `json:"assignmentType"` + Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` + ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` } type MeshBuildingBlockV2DefinitionVersionRef struct { @@ -73,10 +59,10 @@ type MeshBuildingBlockV2Lifecycle struct { } type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs map[string]MeshBuildingBlockV2Output `json:"outputs" tfsdk:"-"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` - Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` } type MeshBuildingBlockV2Client interface { diff --git a/client.go b/client.go index 9a1f366..f9982ac 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.23.0") +var MinMeshStackVersion = version.MustParse("2026.22.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. From 4d82399cb893dd7e2a966c9e1233dce839237099 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 28 May 2026 09:50:42 +0200 Subject: [PATCH 145/200] fix: change input/output structure from array to map CU-86c9p4kcd --- buildingblock_v2.go | 26 ++++++++++++++++++++------ client.go | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 7a6b8bc..9bef5a5 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -36,8 +36,22 @@ type MeshBuildingBlockV2Spec struct { TargetRef MeshBuildingBlockV2TargetRef `json:"targetRef" tfsdk:"target_ref"` DisplayName string `json:"displayName" tfsdk:"display_name"` - Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` - ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` + Inputs map[string]MeshBuildingBlockV2Input `json:"inputs" tfsdk:"-"` + ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` +} + +type MeshBuildingBlockV2Input struct { + Value any `json:"value"` + ValueType string `json:"valueType"` + IsSensitive bool `json:"isSensitive"` + AssignmentType *string `json:"assignmentType"` + UpdateableByConsumer bool `json:"updateableByConsumer"` +} + +type MeshBuildingBlockV2Output struct { + Value any `json:"value"` + ValueType string `json:"valueType"` + AssignmentType *string `json:"assignmentType"` } type MeshBuildingBlockV2DefinitionVersionRef struct { @@ -59,10 +73,10 @@ type MeshBuildingBlockV2Lifecycle struct { } type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` - Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` + Status string `json:"status" tfsdk:"status"` + Outputs map[string]MeshBuildingBlockV2Output `json:"outputs" tfsdk:"-"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` } type MeshBuildingBlockV2Client interface { diff --git a/client.go b/client.go index f9982ac..9a1f366 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.22.0") +var MinMeshStackVersion = version.MustParse("2026.23.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. From 9ac5638f9297764ce33def13a38f689d5f514e5a Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 2 Jun 2026 12:04:05 +0200 Subject: [PATCH 146/200] feat: add purge_on_delete support to meshstack_building_block_v2 Adds a new optional `purge_on_delete` attribute (default `false`) that sends DELETE to the `/purge` endpoint, bypassing the building block's configured deletion run. Useful when a building block is stuck in a non-final state. Deletion now always polls for completion so dependent resources can be cleaned up safely. Co-Authored-By: Claude Sonnet 4.6 --- buildingblock_v2.go | 7 +++++-- internal/mesh_object_client.go | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 9bef5a5..d1d8ab9 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -83,7 +83,7 @@ type MeshBuildingBlockV2Client interface { Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) - Delete(ctx context.Context, uuid string) error + Delete(ctx context.Context, uuid string, purge bool) error } type meshBuildingBlockV2Client struct { @@ -108,7 +108,10 @@ func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingB return c.meshObject.Post(ctx, bb) } -func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string) error { +func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string, purge bool) error { + if purge { + return c.meshObject.Purge(ctx, uuid) + } return c.meshObject.Delete(ctx, uuid) } diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 024ab4e..934e8ae 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -123,6 +123,12 @@ func (c MeshObjectClient[M]) Delete(ctx context.Context, id string) (err error) return } +// Purge removes a meshObject by ID without running any cloud-side cleanup, by calling DELETE /{id}/purge. +func (c MeshObjectClient[M]) Purge(ctx context.Context, id string) (err error) { + _, err = c.doAuthorizedRequest(ctx, http.MethodDelete, c.ApiUrl.JoinPath(id, "purge"), withAccept(c.meshObjectMimeType())) + return +} + // List retrieves all meshObjects with automatic pagination handling. // Accepts optional [RequestOption] parameters for filtering and querying. func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) ([]M, error) { From 24bc91c90988530c0945fdb6ba1b24a457f4359e Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 2 Jun 2026 12:04:26 +0200 Subject: [PATCH 147/200] fix: align BB v2 client with embedded-secret sensitive inputs The meshbuildingblock v2-preview API now returns sensitive inputs as embedded secrets ({"hash":"sha256:..."} with isSensitive:true). Previously the value deserialized to a map and was silently dropped in combined_inputs. This fix adds an UnmarshalJSON to MeshBuildingBlockV2Input that routes sensitive values into the SecretOrAny X-branch and surfaces the hash in state via toResourceModelV2Input. An acceptance test using a STATIC sensitive STRING input covers the fix end-to-end. Co-Authored-By: Claude Sonnet 4.6 --- buildingblock_v2.go | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index d1d8ab9..784bc03 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -2,9 +2,11 @@ package client import ( "context" + "encoding/json" "fmt" "github.com/meshcloud/terraform-provider-meshstack/client/internal" + types "github.com/meshcloud/terraform-provider-meshstack/client/types" ) const ( @@ -41,11 +43,31 @@ type MeshBuildingBlockV2Spec struct { } type MeshBuildingBlockV2Input struct { - Value any `json:"value"` - ValueType string `json:"valueType"` - IsSensitive bool `json:"isSensitive"` - AssignmentType *string `json:"assignmentType"` - UpdateableByConsumer bool `json:"updateableByConsumer"` + Value types.SecretOrAny `json:"value"` + ValueType string `json:"valueType"` + IsSensitive bool `json:"isSensitive"` + AssignmentType *string `json:"assignmentType"` + UpdateableByConsumer bool `json:"updateableByConsumer"` +} + +func (m *MeshBuildingBlockV2Input) UnmarshalJSON(bytes []byte) error { + type wrapped MeshBuildingBlockV2Input + var target wrapped + if err := json.Unmarshal(bytes, &target); err != nil { + return err + } + *m = MeshBuildingBlockV2Input(target) + // Non-sensitive values must live in the Variant's Y branch; the Variant prefers X and + // types.Secret fields are omitempty, so move any accidental X match to Y when not sensitive. + if !m.IsSensitive && m.Value.HasX() { + xJson, err := json.Marshal(m.Value.X) + if err != nil { + return err + } + m.Value.X = types.Secret{} + return json.Unmarshal(xJson, &m.Value.Y) + } + return nil } type MeshBuildingBlockV2Output struct { From 359aaaba21854594d2b0d4b43e9c5fae7405034e Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Wed, 3 Jun 2026 16:46:05 +0200 Subject: [PATCH 148/200] feat: add ALL capability to building block runner implementation types The meshStack backend now models runner capabilities and building block types as separate enums, with runners gaining an additional ALL value that allows a single runner to handle building blocks of any implementation type. Co-Authored-By: Claude Sonnet 4.6 --- buildingblock_runner.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buildingblock_runner.go b/buildingblock_runner.go index e14bbaa..ba63140 100644 --- a/buildingblock_runner.go +++ b/buildingblock_runner.go @@ -15,6 +15,7 @@ const ( MeshBuildingBlockRunnerImplementationTypeGitlabPipeline MeshBuildingBlockRunnerImplementationType = "GITLAB_PIPELINE" MeshBuildingBlockRunnerImplementationTypeAzureDevopsPipeline MeshBuildingBlockRunnerImplementationType = "AZURE_DEVOPS_PIPELINE" MeshBuildingBlockRunnerImplementationTypeManual MeshBuildingBlockRunnerImplementationType = "MANUAL" + MeshBuildingBlockRunnerImplementationTypeAll MeshBuildingBlockRunnerImplementationType = "ALL" ) var MeshBuildingBlockRunnerImplementationTypes = []string{ @@ -23,6 +24,7 @@ var MeshBuildingBlockRunnerImplementationTypes = []string{ string(MeshBuildingBlockRunnerImplementationTypeGitlabPipeline), string(MeshBuildingBlockRunnerImplementationTypeAzureDevopsPipeline), string(MeshBuildingBlockRunnerImplementationTypeManual), + string(MeshBuildingBlockRunnerImplementationTypeAll), } type BuildingBlockRunnerRef struct { From 0e53c9f074dd8495133c776aab1aed646bdddcb0 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 10 Jun 2026 08:26:45 +0200 Subject: [PATCH 149/200] chore: bump to v0.22.0 release, require newest meshstack --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index 9a1f366..956b5aa 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.23.0") +var MinMeshStackVersion = version.MustParse("2026.24.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. From fa000baf7893427fd7bec54c933c290788c2a6ef Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 16 Jun 2026 10:13:41 +0200 Subject: [PATCH 150/200] feat: allow MANAGED_BUILDINGBLOCK_SAVE on building block permissions Add MANAGED_BUILDINGBLOCK_SAVE to the BUILDINGBLOCK_SAVE permission row so the client-side permission validator accepts it when configuring API key permissions. This matches the backend, which now allows assigning this platform-operator authority to API keys (e.g. for setting operator inputs on building blocks across workspaces). BD-2463 Co-Authored-By: Claude Opus 4.8 (1M context) --- api_key_permissions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_key_permissions.go b/api_key_permissions.go index 492315b..c2433d9 100644 --- a/api_key_permissions.go +++ b/api_key_permissions.go @@ -92,7 +92,7 @@ var Permissions = ApiKeyPermissions{ { {"BUILDINGBLOCK_DELETE", "ADM_BUILDINGBLOCK_DELETE"}, {"BUILDINGBLOCK_LIST", "ADM_BUILDINGBLOCK_LIST", "MANAGED_BUILDINGBLOCK_LIST"}, - {"BUILDINGBLOCK_SAVE", "ADM_BUILDINGBLOCK_SAVE"}, + {"BUILDINGBLOCK_SAVE", "ADM_BUILDINGBLOCK_SAVE", "MANAGED_BUILDINGBLOCK_SAVE"}, }, // Building Block Definitions { From 745865e0c1fc834fa5e2f3bbe34f27c792a07222 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Mon, 15 Jun 2026 12:34:00 +0200 Subject: [PATCH 151/200] feat: add support for EntraId meshIntegrations --- integration_config.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/integration_config.go b/integration_config.go index 5b5b680..e88677f 100644 --- a/integration_config.go +++ b/integration_config.go @@ -16,6 +16,7 @@ var ( MeshIntegrationConfigTypeGithub = MeshIntegrationConfigTypes.Entry("github") MeshIntegrationConfigTypeGitlab = MeshIntegrationConfigTypes.Entry("gitlab") MeshIntegrationConfigTypeAzureDevops = MeshIntegrationConfigTypes.Entry("azuredevops") + MeshIntegrationConfigTypeEntraId = MeshIntegrationConfigTypes.Entry("entraid") ) type MeshIntegrationGithubConfig struct { @@ -38,11 +39,19 @@ type MeshIntegrationAzureDevopsConfig struct { RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` } +type MeshIntegrationEntraIdConfig struct { + TenantId string `json:"tenantId" tfsdk:"tenant_id"` + ClientId string `json:"clientId" tfsdk:"client_id"` + ClientSecret types.Secret `json:"clientSecret" tfsdk:"client_secret"` + RedirectUrl *string `json:"redirectUrl,omitempty" tfsdk:"redirect_url"` +} + type MeshIntegrationConfig struct { Type enum.Entry[MeshIntegrationConfigType] `json:"type" tfsdk:"-"` Github *MeshIntegrationGithubConfig `json:"github,omitempty" tfsdk:"github"` Gitlab *MeshIntegrationGitlabConfig `json:"gitlab,omitempty" tfsdk:"gitlab"` AzureDevops *MeshIntegrationAzureDevopsConfig `json:"azuredevops,omitempty" tfsdk:"azuredevops"` + EntraId *MeshIntegrationEntraIdConfig `json:"entraid,omitempty" tfsdk:"entraid"` } func (m MeshIntegrationConfig) InferTypeFromNonNilField() (result enum.Entry[MeshIntegrationConfigType]) { @@ -57,6 +66,7 @@ func (m MeshIntegrationConfig) InferTypeFromNonNilField() (result enum.Entry[Mes setResultIfNotNil(MeshIntegrationConfigTypeGithub, m.Github) setResultIfNotNil(MeshIntegrationConfigTypeGitlab, m.Gitlab) setResultIfNotNil(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops) + setResultIfNotNil(MeshIntegrationConfigTypeEntraId, m.EntraId) if len(result) == 0 { panic("cannot infer config type") } From 5d89105f3eb859c209e89ec22a740bdfa881bc45 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 1 Jun 2026 14:45:53 +0200 Subject: [PATCH 152/200] refactor: generic DoRequest/DoAuthorizedRequest http client API Replaces doRequest/doAuthorizedRequest + GetMeshInfo/unmarshalBody helpers with a generic DoRequest[R]/DoAuthorizedRequest[R] API and adds WithPathElems JoinPath support. Strengthens the PATCH non-retry test to assert exactly one attempt against a retryable 502 response. Co-Authored-By: Claude Sonnet 4.6 --- buildingblock_v2.go | 5 +- client.go | 28 +++++++---- internal/auth.go | 4 +- internal/http_client.go | 74 +++++++++++++++-------------- internal/http_client_test.go | 85 ++++++++++++++++++++-------------- internal/mesh_object_client.go | 34 +++++++------- internal/options.go | 12 ++++- 7 files changed, 140 insertions(+), 102 deletions(-) diff --git a/buildingblock_v2.go b/buildingblock_v2.go index 784bc03..3a806a2 100644 --- a/buildingblock_v2.go +++ b/buildingblock_v2.go @@ -131,10 +131,11 @@ func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingB } func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string, purge bool) error { + var options []internal.RequestOption if purge { - return c.meshObject.Purge(ctx, uuid) + options = append(options, internal.WithPathElems("purge")) } - return c.meshObject.Delete(ctx, uuid) + return c.meshObject.Delete(ctx, uuid, options...) } func (bb *MeshBuildingBlockV2) CreateSuccessful() (done bool, err error) { diff --git a/client.go b/client.go index 956b5aa..1c3c194 100644 --- a/client.go +++ b/client.go @@ -64,15 +64,8 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza }, ) - // Check meshStack version compatibility - if meshInfo, err := httpClient.GetMeshInfo(ctx); err != nil { - return Client{}, fmt.Errorf("failed to retrieve meshStack version information from /mesh/info endpoint: %w", err) - } else if meshInfo.Version.Less(MinMeshStackVersion) { - skipVersionCheck := os.Getenv("MESHSTACK_SKIP_VERSION_CHECK") == "true" - - if !skipVersionCheck { - return Client{}, fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) - } + if err := checkMeshVersion(ctx, httpClient); err != nil { + return Client{}, err } return Client{ @@ -100,3 +93,20 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza WorkspaceUserBinding: newWorkspaceUserBindingClient(ctx, httpClient), }, nil } + +func checkMeshVersion(ctx context.Context, httpClient internal.HttpClient) error { + type MeshInfo struct { + Version version.Version `json:"version"` + } + + meshInfoEndpoint := httpClient.RootUrl.JoinPath("/mesh/info") + if meshInfo, err := internal.DoRequest[MeshInfo](ctx, httpClient, "GET", meshInfoEndpoint); err != nil { + return fmt.Errorf("failed to retrieve meshStack version information from %s endpoint: %w", meshInfoEndpoint, err) + } else if meshInfo.Version.Less(MinMeshStackVersion) { + if os.Getenv("MESHSTACK_SKIP_VERSION_CHECK") == "true" { + return nil + } + return fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) + } + return nil +} diff --git a/internal/auth.go b/internal/auth.go index dccc658..8634100 100644 --- a/internal/auth.go +++ b/internal/auth.go @@ -64,8 +64,8 @@ func (auth *clientSecretAuthorization) ensureValidToken(ctx context.Context, cli ExpireSec int `json:"expires_in"` } - loginResult, err := unmarshalBody[loginResponse](client.doRequest(ctx, http.MethodPost, loginApiUrl, - withPayload(loginRequest{ClientId: auth.ClientId, ClientSecret: auth.ClientSecret}, "application/json")), + loginResult, err := DoRequest[loginResponse](ctx, client, http.MethodPost, loginApiUrl, + withPayload(loginRequest{ClientId: auth.ClientId, ClientSecret: auth.ClientSecret}, "application/json"), ) if err != nil { return fmt.Errorf("login at %s with client id '%s' failed: %w", loginApiUrl, auth.ClientId, err) diff --git a/internal/http_client.go b/internal/http_client.go index 7b8c149..f4190b9 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -8,10 +8,9 @@ import ( "io" "net/http" "net/url" + "reflect" "slices" "time" - - "github.com/meshcloud/terraform-provider-meshstack/client/version" ) // NewHttpClient creates a new client with an underlying http.Client being a pointer to be modified by WithRetry. @@ -27,7 +26,39 @@ type HttpClient struct { Authorization Authorization } -func (c HttpClient) doRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { +func DoAuthorizedRequest[R any](ctx context.Context, c HttpClient, method string, url *url.URL, options ...RequestOption) (result R, err error) { + if c.Authorization == nil { + return result, fmt.Errorf("cannot do authorized request with unconfigured authorization") + } + authHeader, err := c.Authorization.Header(ctx, c) + if err != nil { + return result, err + } + return DoRequest[R](ctx, c, method, url, append(options, withHeader("Authorization", authHeader))...) +} + +func DoRequest[R any](ctx context.Context, c HttpClient, method string, url *url.URL, options ...RequestOption) (result R, err error) { + var body []byte + body, err = c.doRequest(ctx, method, url, options) + if err != nil { + return + } + if len(body) == 0 { + // An empty body is expected only for no-content calls, which are typed DoRequest[any] (e.g. + // trigger-run, delete) and ignore the result. For a call that expects an object (a pointer or a + // concrete struct), an empty 2xx body is unexpected — fail loudly instead of returning a nil/zero + // value that the caller would dereference or mistake for a 404/"not found". + if t := reflect.TypeFor[R](); t.Kind() == reflect.Interface && t.NumMethod() == 0 { + return + } + err = fmt.Errorf("unexpected empty response body from %s %s", method, url) + return + } + err = json.Unmarshal(body, &result) + return +} + +func (c HttpClient) doRequest(ctx context.Context, method string, url *url.URL, options []RequestOption) ([]byte, error) { options = slices.Insert(options, 0, withHeader("User-Agent", c.UserAgent), ) @@ -49,17 +80,6 @@ func (c HttpClient) doRequest(ctx context.Context, method string, url *url.URL, return c.readBodyAndCheckSuccess(ctx, res) } -func (c HttpClient) doAuthorizedRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) ([]byte, error) { - if c.Authorization == nil { - return nil, fmt.Errorf("authorization is not configured") - } - authHeader, err := c.Authorization.Header(ctx, c) - if err != nil { - return nil, err - } - return c.doRequest(ctx, method, url, append(options, withHeader("Authorization", authHeader))...) -} - func (c HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Response) ([]byte, error) { responseBody, err := io.ReadAll(res.Body) if err != nil { @@ -78,6 +98,10 @@ func (c HttpClient) readBodyAndCheckSuccess(ctx context.Context, res *http.Respo } func (c HttpClient) buildRequest(ctx context.Context, method string, url url.URL, opts requestOptions) (*http.Request, error) { + if len(opts.extraPathElems) > 0 { + url = *url.JoinPath(opts.extraPathElems...) + } + if len(opts.urlQueryParams) > 0 { query := url.Query() for k, v := range opts.urlQueryParams { @@ -104,25 +128,3 @@ func (c HttpClient) buildRequest(ctx context.Context, method string, url url.URL Log.Debug(ctx, "request", "url", req.URL.String(), "method", req.Method, "headers", loggedHeaders(req.Header), "body", loggedBody{requestBody}) return req, err } - -// unmarshalBody is a generic helper to unmarshal a JSON response. -// It intentionally takes err as second argument to match doAuthorizedRequest and doRequest signatures. -func unmarshalBody[T any](body []byte, err error) (*T, error) { - if err != nil { - return nil, err - } - var target T - if err := json.Unmarshal(body, &target); err != nil { - return nil, fmt.Errorf("cannot unmarshal body: %w", err) - } - return &target, nil -} - -type MeshInfo struct { - Version version.Version `json:"version"` -} - -func (c HttpClient) GetMeshInfo(ctx context.Context) (*MeshInfo, error) { - meshInfoUrl := c.RootUrl.JoinPath("/mesh/info") - return unmarshalBody[MeshInfo](c.doRequest(ctx, "GET", meshInfoUrl)) -} diff --git a/internal/http_client_test.go b/internal/http_client_test.go index b14d625..ea38a0f 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -12,32 +12,46 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/meshcloud/terraform-provider-meshstack/client/version" ) func TestHttpClient(t *testing.T) { - t.Run("GetMeshInfo success", func(t *testing.T) { + t.Run("DoRequest success", func(t *testing.T) { testLogger := installTestLogger(t) client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(http.StatusOK) - _, _ = resp.Write([]byte(`{"version": "2026.10.0"}`)) - - assert.Equal(t, "/mesh/info", req.URL.Path) + _, _ = resp.Write([]byte(`"some-answer"`)) + assert.Equal(t, "/get", req.URL.Path) assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "test-agent", req.Header.Get("User-Agent")) }) - info, err := client.GetMeshInfo(t.Context()) + resp, err := DoRequest[string](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("get")) require.NoError(t, err) - assert.Equal(t, &MeshInfo{Version: version.Version{Major: 2026, Minor: 10}}, info) + assert.Equal(t, "some-answer", resp) assert.Equal(t, []string{ - fmt.Sprintf("request [url %s/mesh/info method GET headers User-Agent=test-agent body ]", client.RootUrl), - "response [status 200 body {\n \"version\": \"2026.10.0\"\n}]", + fmt.Sprintf("request [url %s/get method GET headers User-Agent=test-agent body ]", client.RootUrl), + `response [status 200 body "some-answer"]`, }, testLogger.Debugs) assert.Empty(t, testLogger.Warns) }) - t.Run("GetMeshInfo with successful retry", func(t *testing.T) { + t.Run("DoRequest object call with empty 2xx body errors", func(t *testing.T) { + client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusOK) // 200 with no body + }) + _, err := DoRequest[*string](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("get")) + require.Error(t, err) + assert.ErrorContains(t, err, "unexpected empty response body") + }) + + t.Run("DoRequest no-content call (any) tolerates an empty 2xx body", func(t *testing.T) { + client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusAccepted) // e.g. trigger-run / delete: empty body by design + }) + _, err := DoRequest[any](t.Context(), client, http.MethodPost, client.RootUrl.JoinPath("trigger-run")) + require.NoError(t, err) + }) + + t.Run("DoRequest with successful retry", func(t *testing.T) { for _, retryableStatusCode := range []int{429, 502, 503, 504} { t.Run(fmt.Sprintf("after code %d", retryableStatusCode), func(t *testing.T) { nowUTC := mockTimeNowAsUTC(t) @@ -58,7 +72,7 @@ func TestHttpClient(t *testing.T) { _, _ = resp.Write([]byte(`{}`)) }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff}) - _, err := client.GetMeshInfo(t.Context()) + _, err := DoRequest[any](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("get")) require.NoError(t, err) if retryableStatusCode == 429 { assert.Equal(t, 0, retryTestBackoff.Called) @@ -66,53 +80,56 @@ func TestHttpClient(t *testing.T) { assert.Equal(t, 1, retryTestBackoff.Called) } assert.Equal(t, []string{ - fmt.Sprintf("retrying request [status %d method GET path /mesh/info attempt 1/3 waitTime 1s]", retryableStatusCode), + fmt.Sprintf("retrying request [status %d method GET path /get attempt 1/3 waitTime 1s]", retryableStatusCode), }, testLogger.Warns) }) } }) - t.Run("GetMeshInfo with 2 retries exhausted", func(t *testing.T) { + t.Run("DoRequest with 2 retries exhausted", func(t *testing.T) { testLogger := installTestLogger(t) retryTestBackoff := retryTestBackoff{} client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(502) }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff}) - _, err := client.GetMeshInfo(t.Context()) + _, err := DoRequest[any](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("get")) var httpErr HttpError require.ErrorAs(t, err, &httpErr) assert.Equal(t, 502, httpErr.StatusCode) assert.Equal(t, 2, retryTestBackoff.Called) assert.Equal(t, []string{ - "retrying request [status 502 method GET path /mesh/info attempt 1/2 waitTime 0s]", - "retrying request [status 502 method GET path /mesh/info attempt 2/2 waitTime 0s]", + "retrying request [status 502 method GET path /get attempt 1/2 waitTime 0s]", + "retrying request [status 502 method GET path /get attempt 2/2 waitTime 0s]", }, testLogger.Warns) assert.Equal(t, []string{ - fmt.Sprintf("request [url %s/mesh/info method GET headers User-Agent=test-agent body ]", client.RootUrl), + fmt.Sprintf("request [url %s/get method GET headers User-Agent=test-agent body ]", client.RootUrl), "response [status 502 body ]", }, testLogger.Debugs) }) - t.Run("GetMeshInfo with context cancelled during backoff", func(t *testing.T) { + t.Run("DoRequest with context cancelled during backoff", func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(502) cancel() // cancel context so the backoff wait is interrupted }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff{WaitTime: 10 * time.Second}}) - _, err := client.GetMeshInfo(ctx) + _, err := DoRequest[any](ctx, client, http.MethodGet, client.RootUrl.JoinPath("get")) require.ErrorIs(t, err, context.Canceled) }) - t.Run("doRequest with PATCH (not retried)", func(t *testing.T) { + t.Run("DoRequest with PATCH (not retried)", func(t *testing.T) { + attempts := 0 client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { - resp.WriteHeader(200) + attempts++ + resp.WriteHeader(502) }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff{WaitTime: 10 * time.Second}}) - _, err := client.doRequest(t.Context(), http.MethodPatch, client.RootUrl) - require.NoError(t, err) + _, err := DoRequest[any](t.Context(), client, http.MethodPatch, client.RootUrl) + require.Error(t, err) + assert.Equal(t, 1, attempts, "PATCH must not be retried") }) - t.Run("doRequest with PUT replays body on retry", func(t *testing.T) { + t.Run("DoRequest with PUT replays body on retry", func(t *testing.T) { attempt := 0 client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { body, _ := io.ReadAll(req.Body) @@ -124,22 +141,22 @@ func TestHttpClient(t *testing.T) { } resp.WriteHeader(200) }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff{}}) - _, err := client.doRequest(t.Context(), http.MethodPut, client.RootUrl, withPayload(map[string]string{"key": "value"}, "application/json")) + _, err := DoRequest[any](t.Context(), client, http.MethodPut, client.RootUrl, withPayload(map[string]string{"key": "value"}, "application/json")) require.NoError(t, err) assert.Equal(t, 2, attempt) }) - t.Run("doAuthorizedRequest with BearerTokenAuthorization", func(t *testing.T) { + t.Run("DoAuthorizedRequest with BearerTokenAuthorization", func(t *testing.T) { client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { assert.Equal(t, "Bearer my-static-token", req.Header.Get("Authorization")) resp.WriteHeader(http.StatusAccepted) }) client.Authorization = BearerTokenAuthorization{Token: "my-static-token"} - _, err := client.doAuthorizedRequest(t.Context(), http.MethodPost, client.RootUrl.JoinPath("create"), withPayload("content", "text/plain")) + _, err := DoAuthorizedRequest[any](t.Context(), client, http.MethodPost, client.RootUrl.JoinPath("create"), withPayload("content", "text/plain")) require.NoError(t, err) }) - t.Run("doAuthorizedRequest with clientSecretAuthorization and retries", func(t *testing.T) { + t.Run("DoAuthorizedRequest with clientSecretAuthorization and retries", func(t *testing.T) { t.Run("succeeds after second attempt", func(t *testing.T) { retryTestBackoff := retryTestBackoff{} requestsSeen := map[string]int{} // key is request path @@ -164,16 +181,16 @@ func TestHttpClient(t *testing.T) { } }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff, WhitelistedPaths: map[string][]string{http.MethodPost: {"/login"}}}) client.Authorization = NewClientSecretAuthorization("login", "test-client", "test-client-secret") - resp, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + resp, err := DoAuthorizedRequest[any](t.Context(), client, http.MethodPut, client.RootUrl.JoinPath("edit")) require.NoError(t, err) - require.NotNil(t, resp) + _ = resp assert.Equal(t, map[string]int{ "/login": 2, "/edit": 2, }, requestsSeen) t.Run("expired token is refreshed with relogin", func(t *testing.T) { - _, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + _, err := DoAuthorizedRequest[any](t.Context(), client, http.MethodPut, client.RootUrl.JoinPath("edit")) require.NoError(t, err) assert.Equal(t, 2, retryTestBackoff.Called) assert.Equal(t, map[string]int{ @@ -215,7 +232,7 @@ func TestHttpClient(t *testing.T) { } }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff, WhitelistedPaths: map[string][]string{http.MethodPost: {"/login"}}}) client.Authorization = NewClientSecretAuthorization("login", "test-client", "test-client-secret") - _, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + _, err := DoAuthorizedRequest[any](t.Context(), client, http.MethodPut, client.RootUrl.JoinPath("edit")) require.NoError(t, err) assert.Equal(t, map[string]int{ "/login": 2, // 1st: 502, 2nd: 307 redirect @@ -231,7 +248,7 @@ func TestHttpClient(t *testing.T) { resp.WriteHeader(503) }), RetryOptions{MaxRetries: 2, Backoff: &retryTestBackoff, WhitelistedPaths: map[string][]string{http.MethodPost: {"/login"}}}) client.Authorization = NewClientSecretAuthorization("login", "test-client", "test-client-secret") - _, err := client.doAuthorizedRequest(t.Context(), http.MethodPut, client.RootUrl.JoinPath("edit")) + _, err := DoAuthorizedRequest[any](t.Context(), client, http.MethodPut, client.RootUrl.JoinPath("edit")) require.ErrorContains(t, err, fmt.Sprintf("login at %s/login with client id 'test-client' failed", client.RootUrl)) var httpErr HttpError require.ErrorAs(t, err, &httpErr) diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 934e8ae..0fc5b0b 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -69,13 +69,13 @@ func pluralizeKind(kind string) string { return kind + "s" } -func (c MeshObjectClient[M]) meshObjectMimeType() string { +func (c MeshObjectClient[M]) MeshObjectMimeType() string { return fmt.Sprintf("application/vnd.meshcloud.api.%s.%s.hal+json", c.Kind, c.ApiVersion) } // Get retrieves a meshObject by ID. Returns nil if not found. func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (resp *M, err error) { - resp, err = unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType()))) + resp, err = DoAuthorizedRequest[*M](ctx, c.HttpClient, http.MethodGet, c.ApiUrl.JoinPath(id), WithAccept(c.MeshObjectMimeType())) if httpErr, ok := errors.AsType[HttpError](err); ok && httpErr.IsNotFound() { return nil, nil } @@ -84,14 +84,20 @@ func (c MeshObjectClient[M]) Get(ctx context.Context, id string) (resp *M, err e // Post creates a new meshObject with the given payload. // Automatically injects apiVersion and kind into the JSON payload. -func (c MeshObjectClient[M]) Post(ctx context.Context, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPost, c.ApiUrl, c.withMeshObjectPayload(payload))) +func (c MeshObjectClient[M]) Post(ctx context.Context, payload any, options ...RequestOption) (*M, error) { + return DoAuthorizedRequest[*M]( + ctx, + c.HttpClient, + http.MethodPost, + c.ApiUrl, + append(options, c.withMeshObjectPayload(payload))..., + ) } // Put updates an existing meshObject by ID with the given payload. // Automatically injects apiVersion and kind into the JSON payload. func (c MeshObjectClient[M]) Put(ctx context.Context, id string, payload any) (*M, error) { - return unmarshalBody[M](c.doAuthorizedRequest(ctx, http.MethodPut, c.ApiUrl.JoinPath(id), c.withMeshObjectPayload(payload))) + return DoAuthorizedRequest[*M](ctx, c.HttpClient, http.MethodPut, c.ApiUrl.JoinPath(id), c.withMeshObjectPayload(payload)) } // withMeshObjectPayload returns a RequestOption that sets the payload with apiVersion and kind injected, @@ -114,18 +120,12 @@ func (c MeshObjectClient[M]) withMeshObjectPayload(payload any) RequestOption { m["apiVersion"] = c.ApiVersion m["kind"] = c.Kind - return withPayload(m, c.meshObjectMimeType()) + return withPayload(m, c.MeshObjectMimeType()) } // Delete removes a meshObject by ID. -func (c MeshObjectClient[M]) Delete(ctx context.Context, id string) (err error) { - _, err = c.doAuthorizedRequest(ctx, http.MethodDelete, c.ApiUrl.JoinPath(id), withAccept(c.meshObjectMimeType())) - return -} - -// Purge removes a meshObject by ID without running any cloud-side cleanup, by calling DELETE /{id}/purge. -func (c MeshObjectClient[M]) Purge(ctx context.Context, id string) (err error) { - _, err = c.doAuthorizedRequest(ctx, http.MethodDelete, c.ApiUrl.JoinPath(id, "purge"), withAccept(c.meshObjectMimeType())) +func (c MeshObjectClient[M]) Delete(ctx context.Context, id string, options ...RequestOption) (err error) { + _, err = DoAuthorizedRequest[any](ctx, c.HttpClient, http.MethodDelete, c.ApiUrl.JoinPath(id), append(options, WithAccept(c.MeshObjectMimeType()))...) return } @@ -144,10 +144,10 @@ func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) Number int `json:"number"` } `json:"page"` } - response, err := unmarshalBody[paginatedResponse](c.doAuthorizedRequest(ctx, http.MethodGet, c.ApiUrl, append(options, - withAccept(c.meshObjectMimeType()), + response, err := DoAuthorizedRequest[paginatedResponse](ctx, c.HttpClient, http.MethodGet, c.ApiUrl, append(options, + WithAccept(c.MeshObjectMimeType()), WithUrlQuery("page", pageNumber), - )...)) + )...) if err != nil { return result, fmt.Errorf("error getting page %d: %w", pageNumber, err) } else if items, ok := response.Embedded[embeddedKey]; !ok { diff --git a/internal/options.go b/internal/options.go index 4726dde..26d41cf 100644 --- a/internal/options.go +++ b/internal/options.go @@ -11,6 +11,7 @@ type ( requestOptions struct { urlQueryParams map[string]string + extraPathElems []string requestPayload any requestModifiers []requestModifier } @@ -34,13 +35,20 @@ func WithUrlQuery(key string, value any) RequestOption { } } +// WithPathElems appends path elements to the request URL path. +func WithPathElems(pathElems ...string) RequestOption { + return func(opts *requestOptions) { + opts.extraPathElems = append(opts.extraPathElems, pathElems...) + } +} + func appendRequestModifier(modifier requestModifier) RequestOption { return func(opts *requestOptions) { opts.requestModifiers = append(opts.requestModifiers, modifier) } } -func withAccept(accept string) RequestOption { +func WithAccept(accept string) RequestOption { return withHeader("Accept", accept) } @@ -52,7 +60,7 @@ func withHeader(key, value string) RequestOption { func withPayload(payload any, contentType string) RequestOption { return func(opts *requestOptions) { - withAccept(contentType)(opts) + WithAccept(contentType)(opts) withHeader("Content-Type", contentType)(opts) opts.requestPayload = payload } From 4481ff511b009212de6b56e402fc63e54ffc5ba5 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 1 Jun 2026 14:46:23 +0200 Subject: [PATCH 153/200] =?UTF-8?q?refactor:=20normalize=20buildingblock?= =?UTF-8?q?=20=E2=86=92=20building=5Fblock=20file=20&=20symbol=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename all non-v1 building-block files from buildingblock_* to building_block_* to match the resource name meshstack_building_block. De-collide v1 symbols (NewBuildingblockResource / buildingblockResource) to free the CamelCase names for the new resource, and update the v1 deprecation message target from meshstack_building_block_v3 to meshstack_building_block. Rename-only; no behaviour change. Co-Authored-By: Claude Sonnet 4.6 --- ...lock_definition.go => building_block_definition.go | 0 ...version.go => building_block_definition_version.go | 0 ...uilding_block_definition_version_implementation.go | 0 ...st.go => building_block_definition_version_test.go | 0 buildingblock_runner.go => building_block_runner.go | 0 buildingblock_v2.go => building_block_v2.go | 0 buildingblock_v2_test.go => building_block_v2_test.go | 11 +++++------ 7 files changed, 5 insertions(+), 6 deletions(-) rename buildingblock_definition.go => building_block_definition.go (100%) rename buildingblock_definition_version.go => building_block_definition_version.go (100%) rename buildingblock_definition_version_implementation.go => building_block_definition_version_implementation.go (100%) rename buildingblock_definition_version_test.go => building_block_definition_version_test.go (100%) rename buildingblock_runner.go => building_block_runner.go (100%) rename buildingblock_v2.go => building_block_v2.go (100%) rename buildingblock_v2_test.go => building_block_v2_test.go (81%) diff --git a/buildingblock_definition.go b/building_block_definition.go similarity index 100% rename from buildingblock_definition.go rename to building_block_definition.go diff --git a/buildingblock_definition_version.go b/building_block_definition_version.go similarity index 100% rename from buildingblock_definition_version.go rename to building_block_definition_version.go diff --git a/buildingblock_definition_version_implementation.go b/building_block_definition_version_implementation.go similarity index 100% rename from buildingblock_definition_version_implementation.go rename to building_block_definition_version_implementation.go diff --git a/buildingblock_definition_version_test.go b/building_block_definition_version_test.go similarity index 100% rename from buildingblock_definition_version_test.go rename to building_block_definition_version_test.go diff --git a/buildingblock_runner.go b/building_block_runner.go similarity index 100% rename from buildingblock_runner.go rename to building_block_runner.go diff --git a/buildingblock_v2.go b/building_block_v2.go similarity index 100% rename from buildingblock_v2.go rename to building_block_v2.go diff --git a/buildingblock_v2_test.go b/building_block_v2_test.go similarity index 81% rename from buildingblock_v2_test.go rename to building_block_v2_test.go index 312887d..dc221f9 100644 --- a/buildingblock_v2_test.go +++ b/building_block_v2_test.go @@ -22,7 +22,7 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { { name: "lifecycle state DELETED", bb: &MeshBuildingBlockV2{ - Status: MeshBuildingBlockV2Status{ + Status: &MeshBuildingBlockV2Status{ Lifecycle: MeshBuildingBlockV2Lifecycle{State: BUILDING_BLOCK_LIFECYCLE_STATE_DELETED}, }, }, @@ -32,9 +32,9 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { { name: "status FAILED during deletion", bb: &MeshBuildingBlockV2{ - Metadata: MeshBuildingBlockV2Metadata{Uuid: "test-uuid"}, - Status: MeshBuildingBlockV2Status{ - Status: BUILDING_BLOCK_STATUS_FAILED, + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{ + Status: BuildingBlockStatusFailed, }, }, wantDone: false, @@ -43,8 +43,7 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { { name: "still in progress (MARKED_FOR_DELETION lifecycle, non-failed status)", bb: &MeshBuildingBlockV2{ - Status: MeshBuildingBlockV2Status{ - Status: BUILDING_BLOCK_STATUS_IN_PROGRESS, + Status: &MeshBuildingBlockV2Status{ Lifecycle: MeshBuildingBlockV2Lifecycle{State: BUILDING_BLOCK_LIFECYCLE_STATE_MARKED_FOR_DELETION}, }, }, From cd3a815a15ae5908fbb441ced6d608565c970fb3 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 1 Jun 2026 14:47:35 +0200 Subject: [PATCH 154/200] feat: implement meshstack_building_block resource Adds the meshstack_building_block resource (v3), superseding v2 with: - In-place updates via PUT + explicit trigger-run; no destroy+recreate. Changes to content_hash, inputs, or parent_building_blocks trigger a rerun; in-place version upgrades are supported via PUT - Sensitive input support (value_string_sensitive/value_code_sensitive), preserving secrets across upgrades and avoiding phantom drift from null USER_INPUT rows - wait_for_completion: polls up to 30 min; WAITING_FOR_* states produce actionable warnings; preserved across reads/import (null vs false) - run-log diagnostics: on poll failure addRunFailureDiagnostics surfaces step-level logs as Terraform warnings - MoveState from meshstack_buildingblock (v1) and meshstack_building_block_v2 - target_ref validators (meshTenant requires uuid, meshWorkspace requires name) - Soft-delete-aware Read: a deleted building block is removed from state - Hardens the building_block_v2 client/mock/data-source: nil-pointer guards, ABORTED + nil Status handling in CreateSuccessful, sensitive input reads in the v2 data source, mock deep-copy fidelity - Fixes CHANGELOG, truncated all_inputs.value doc, purge description, operator-input test assertions, secret docs version reference - Regenerates provider docs (building_block.md replaces building_block_v3.md) Co-Authored-By: Claude Sonnet 4.6 --- building_block_run.go | 60 +++++++++++ building_block_v2.go | 206 +++++++++++++++++++++++++++----------- building_block_v2_test.go | 116 ++++++++++++++++++++- client.go | 2 + client_kind.go | 2 + internal/http_error.go | 5 + types/clienttypes.go | 4 +- 7 files changed, 330 insertions(+), 65 deletions(-) create mode 100644 building_block_run.go diff --git a/building_block_run.go b/building_block_run.go new file mode 100644 index 0000000..2083188 --- /dev/null +++ b/building_block_run.go @@ -0,0 +1,60 @@ +package client + +import ( + "context" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + +type MeshBuildingBlockRun struct { + Metadata MeshBuildingBlockRunMetadata `json:"metadata"` + Spec MeshBuildingBlockRunSpec `json:"spec"` + Status string `json:"status"` +} + +type MeshBuildingBlockRunMetadata struct { + Uuid string `json:"uuid"` + CreatedOn string `json:"createdOn"` +} + +type MeshBuildingBlockRunSpec struct { + RunNumber int64 `json:"runNumber"` + Behavior string `json:"behavior"` +} + +// MeshBuildingBlockRunLogs is the response from the download-logs actions endpoint. +type MeshBuildingBlockRunLogs struct { + Steps []MeshBuildingBlockRunStepLog `json:"steps"` +} + +// MeshBuildingBlockRunStepLog represents a single step's log data. +type MeshBuildingBlockRunStepLog struct { + DisplayName string `json:"displayName"` + Status string `json:"status"` + UserMessage *string `json:"userMessage"` + SystemMessage *string `json:"systemMessage"` +} + +type MeshBuildingBlockRunClient interface { + GetLogs(ctx context.Context, runUuid string) (MeshBuildingBlockRunLogs, error) +} + +type meshBuildingBlockRunClient struct { + meshObject internal.MeshObjectClient[MeshBuildingBlockRun] +} + +func newBuildingBlockRunClient(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockRunClient { + return meshBuildingBlockRunClient{ + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockRun](ctx, httpClient, "v1"), + } +} + +func (c meshBuildingBlockRunClient) GetLogs(ctx context.Context, runUuid string) (MeshBuildingBlockRunLogs, error) { + return internal.DoAuthorizedRequest[MeshBuildingBlockRunLogs]( + ctx, + c.meshObject.HttpClient, + "GET", + c.meshObject.ApiUrl.JoinPath(runUuid, "logs"), + internal.WithAccept(c.meshObject.MeshObjectMimeType()), + ) +} diff --git a/building_block_v2.go b/building_block_v2.go index 3a806a2..8e0d494 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -3,34 +3,47 @@ package client import ( "context" "encoding/json" + "errors" "fmt" + "slices" "github.com/meshcloud/terraform-provider-meshstack/client/internal" - types "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) -const ( - // Building Block Status Constants. - BUILDING_BLOCK_STATUS_WAITING_FOR_DEPENDENT_INPUT = "WAITING_FOR_DEPENDENT_INPUT" - BUILDING_BLOCK_STATUS_WAITING_FOR_OPERATOR_INPUT = "WAITING_FOR_OPERATOR_INPUT" - BUILDING_BLOCK_STATUS_PENDING = "PENDING" - BUILDING_BLOCK_STATUS_IN_PROGRESS = "IN_PROGRESS" - BUILDING_BLOCK_STATUS_SUCCEEDED = "SUCCEEDED" - BUILDING_BLOCK_STATUS_FAILED = "FAILED" - BUILDING_BLOCK_LIFECYCLE_STATE_ACTIVE = "ACTIVE" - BUILDING_BLOCK_LIFECYCLE_STATE_MARKED_FOR_DELETION = "MARKED_FOR_DELETION" - BUILDING_BLOCK_LIFECYCLE_STATE_DELETED = "DELETED" +type BuildingBlockLifecycleState string + +var ( + BuildingBlockLifecycleStates = enum.Enum[BuildingBlockLifecycleState]{} + BuildingBlockLifecycleStateActive = BuildingBlockLifecycleStates.Entry("ACTIVE") + BuildingBlockLifecycleStateMarkedForDeletion = BuildingBlockLifecycleStates.Entry("MARKED_FOR_DELETION") + BuildingBlockLifecycleStateDeleted = BuildingBlockLifecycleStates.Entry("DELETED") +) + +type BuildingBlockStatus string + +var ( + BuildingBlockStatuses = enum.Enum[BuildingBlockStatus]{} + BuildingBlockStatusWaitingForDependentInput = BuildingBlockStatuses.Entry("WAITING_FOR_DEPENDENT_INPUT") + BuildingBlockStatusWaitingForOperatorInput = BuildingBlockStatuses.Entry("WAITING_FOR_OPERATOR_INPUT") + BuildingBlockStatusWaitingForUserInput = BuildingBlockStatuses.Entry("WAITING_FOR_USER_INPUT") + BuildingBlockStatusPending = BuildingBlockStatuses.Entry("PENDING") + BuildingBlockStatusInProgress = BuildingBlockStatuses.Entry("IN_PROGRESS") + BuildingBlockStatusSucceeded = BuildingBlockStatuses.Entry("SUCCEEDED") + BuildingBlockStatusFailed = BuildingBlockStatuses.Entry("FAILED") + BuildingBlockStatusAborted = BuildingBlockStatuses.Entry("ABORTED") ) type MeshBuildingBlockV2 struct { Metadata MeshBuildingBlockV2Metadata `json:"metadata" tfsdk:"metadata"` Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` - Status MeshBuildingBlockV2Status `json:"status" tfsdk:"status"` + Status *MeshBuildingBlockV2Status `json:"status" tfsdk:"status"` } type MeshBuildingBlockV2Metadata struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + Uuid *string `json:"uuid" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` } type MeshBuildingBlockV2Spec struct { @@ -38,46 +51,60 @@ type MeshBuildingBlockV2Spec struct { TargetRef MeshBuildingBlockV2TargetRef `json:"targetRef" tfsdk:"target_ref"` DisplayName string `json:"displayName" tfsdk:"display_name"` - Inputs map[string]MeshBuildingBlockV2Input `json:"inputs" tfsdk:"-"` - ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` + // Inputs as pointer MeshBuildingBlockInput to support mocking secret responses. + Inputs map[string]*MeshBuildingBlockInput `json:"inputs" tfsdk:"inputs"` + ParentBuildingBlocks types.Set[MeshBuildingBlockParent] `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` } -type MeshBuildingBlockV2Input struct { - Value types.SecretOrAny `json:"value"` - ValueType string `json:"valueType"` - IsSensitive bool `json:"isSensitive"` - AssignmentType *string `json:"assignmentType"` - UpdateableByConsumer bool `json:"updateableByConsumer"` +type MeshBuildingBlockInput struct { + Value types.SecretOrAny `json:"value" tfsdk:"value"` + ValueType *enum.Entry[MeshBuildingBlockIOType] `json:"valueType,omitempty" tfsdk:"-"` + AssignmentType enum.Entry[MeshBuildingBlockInputAssignmentType] `json:"assignmentType,omitempty" tfsdk:"-"` + + // If IsSensitive is true, the [types.Variant] (typedef [types.SecretOrAny]) for Value field + // is of [types.Secret] (case [types.Variant.X]). + // Otherwise, the [types.Variant] is of [types.Any] (case [types.Variant.Y]). + // As this is a fallback detection when JSON (un)marshaling, + // types.Any must go second as [types.Variant] intentionally prefers X over Y. + IsSensitive bool `json:"isSensitive" tfsdk:"-"` } -func (m *MeshBuildingBlockV2Input) UnmarshalJSON(bytes []byte) error { - type wrapped MeshBuildingBlockV2Input +func (m *MeshBuildingBlockInput) UnmarshalJSON(bytes []byte) error { + type wrapped MeshBuildingBlockInput var target wrapped if err := json.Unmarshal(bytes, &target); err != nil { return err } - *m = MeshBuildingBlockV2Input(target) - // Non-sensitive values must live in the Variant's Y branch; the Variant prefers X and - // types.Secret fields are omitempty, so move any accidental X match to Y when not sensitive. - if !m.IsSensitive && m.Value.HasX() { - xJson, err := json.Marshal(m.Value.X) - if err != nil { - return err + *m = MeshBuildingBlockInput(target) + switch { + case !m.IsSensitive: + // ensure "any" struct fields never end up in X accidentally, + // as X is only set when IsSensitive is true! + var errs []error + moveXtoYIfPresent := func(v *types.SecretOrAny) { + if v.HasX() { + xJson, err := json.Marshal(v.X) + errs = append(errs, err) + v.X = types.Secret{} + errs = append(errs, json.Unmarshal(xJson, &v.Y)) + } } - m.Value.X = types.Secret{} - return json.Unmarshal(xJson, &m.Value.Y) + moveXtoYIfPresent(&m.Value) + return errors.Join(errs...) + case m.Value.HasY(): + return fmt.Errorf("got sensitive argument or default_value but variant Y is set instead") + default: + return nil } - return nil -} - -type MeshBuildingBlockV2Output struct { - Value any `json:"value"` - ValueType string `json:"valueType"` - AssignmentType *string `json:"assignmentType"` } type MeshBuildingBlockV2DefinitionVersionRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` + // ContentHash is a Terraform-only field (json:"-", never sent to or returned by the backend). + // It lets a config signal that the referenced version's content changed so a rerun is triggered + // even though the version uuid is unchanged. The building_block (v3) resource honors it via the + // shared rerunNeeded predicate used by both ModifyPlan and Update. + ContentHash *string `json:"-" tfsdk:"content_hash"` } type MeshBuildingBlockV2TargetRef struct { @@ -86,26 +113,36 @@ type MeshBuildingBlockV2TargetRef struct { Name *string `json:"name" tfsdk:"name"` } -type MeshBuildingBlockV2Create struct { - Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` -} - type MeshBuildingBlockV2Lifecycle struct { - State string `json:"state" tfsdk:"state"` + State enum.Entry[BuildingBlockLifecycleState] `json:"state" tfsdk:"state"` } type MeshBuildingBlockV2Status struct { - Status string `json:"status" tfsdk:"status"` - Outputs map[string]MeshBuildingBlockV2Output `json:"outputs" tfsdk:"-"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` - Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"lifecycle"` + Status enum.Entry[BuildingBlockStatus] `json:"status" tfsdk:"status"` + Outputs map[string]MeshBuildingBlockOutput `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` + Lifecycle MeshBuildingBlockV2Lifecycle `json:"lifecycle" tfsdk:"-"` + // LatestRunUuid is nil if permissions don't allow reading the run (e.g. because run_transparency is false). + // It tracks the latest *modifying* (apply/destroy) run and excludes dry runs. + LatestRunUuid *string `json:"latestRunUuid" tfsdk:"latest_run_uuid"` + // LatestDryRunUuid is the latest dry (DETECT) run, but only when it is the newest run; nil otherwise. + // Same permission gating and nullability caveat as LatestRunUuid. + LatestDryRunUuid *string `json:"latestDryRunUuid" tfsdk:"latest_dry_run_uuid"` +} + +type MeshBuildingBlockOutput struct { + Value types.Any `json:"value" tfsdk:"value"` + ValueType enum.Entry[MeshBuildingBlockIOType] `json:"valueType" tfsdk:"value_type"` + AssignmentType enum.Entry[MeshBuildingBlockDefinitionOutputAssignmentType] `json:"assignmentType" tfsdk:"assignment_type"` } type MeshBuildingBlockV2Client interface { Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) - Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) + Create(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) + Update(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) Delete(ctx context.Context, uuid string, purge bool) error + TriggerRun(ctx context.Context, uuid string) error } type meshBuildingBlockV2Client struct { @@ -126,10 +163,17 @@ func (c meshBuildingBlockV2Client) ReadFunc(uuid string) func(ctx context.Contex } } -func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { +func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) { return c.meshObject.Post(ctx, bb) } +func (c meshBuildingBlockV2Client) Update(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) { + if bb.Metadata.Uuid == nil { + return nil, fmt.Errorf("cannot update building block without UUID") + } + return c.meshObject.Put(ctx, *bb.Metadata.Uuid, bb) +} + func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string, purge bool) error { var options []internal.RequestOption if purge { @@ -138,14 +182,41 @@ func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string, purg return c.meshObject.Delete(ctx, uuid, options...) } +// IsWaitingForInput reports whether the building block run is paused awaiting +// human or dependency input. Such a run will not progress on its own, so polling +// callers treat it as a terminal (but non-fatal) state and surface a warning. +func (bb *MeshBuildingBlockV2) IsWaitingForInput() bool { + return bb.Status.Status == BuildingBlockStatusWaitingForOperatorInput || + bb.Status.Status == BuildingBlockStatusWaitingForUserInput || + bb.Status.Status == BuildingBlockStatusWaitingForDependentInput +} + +// bbUuidOrUnknown returns the building block UUID for diagnostic messages, or "" if nil. +func bbUuidOrUnknown(bb *MeshBuildingBlockV2) string { + if bb != nil && bb.Metadata.Uuid != nil { + return *bb.Metadata.Uuid + } + return "" +} + func (bb *MeshBuildingBlockV2) CreateSuccessful() (done bool, err error) { switch { case bb == nil: err = fmt.Errorf("building block not found after creation") - case bb.Status.Status == BUILDING_BLOCK_STATUS_FAILED: - err = fmt.Errorf("building block %s reached FAILED state during creation, check the building block run logs in meshStack", bb.Metadata.Uuid) - case bb.Status.Status == BUILDING_BLOCK_STATUS_SUCCEEDED: + case bb.Status == nil: + // no status yet — keep polling + case bb.Status.Status == BuildingBlockStatusFailed, + bb.Status.Status == BuildingBlockStatusAborted: + err = fmt.Errorf("building block %s reached %s state, check run logs in meshStack", bbUuidOrUnknown(bb), bb.Status.Status) + case bb.IsWaitingForInput(): + // Paused awaiting input — stop polling so the caller can surface a warning. + done = true + case bb.Status.Status == BuildingBlockStatusSucceeded: done = true + case !slices.Contains(BuildingBlockStatuses, bb.Status.Status): + // Unrecognized status: fail fast instead of polling to the timeout — the backend returned a + // status this provider version does not know about (provider may be out of date). + err = fmt.Errorf("unknown building block status %q for building block %s; provider may be out of date", bb.Status.Status, bbUuidOrUnknown(bb)) } return } @@ -153,13 +224,28 @@ func (bb *MeshBuildingBlockV2) CreateSuccessful() (done bool, err error) { func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) { switch { case bb == nil: - // Expected when receiving a 404 (hard deletion), default behavior until meshStack v2026.20.0. - // For versions higher than that, we get a building block back with a lifecycle state to inspect. + // 404: the block was hard-removed (e.g. its definition was deleted too); treat as done. done = true - case bb.Status.Lifecycle.State == BUILDING_BLOCK_LIFECYCLE_STATE_DELETED: + case bb.Status != nil && bb.Status.Lifecycle.State == BuildingBlockLifecycleStateDeleted: + // Soft delete: once deletion completes the backend keeps returning the block with lifecycle + // DELETED (it does not 404), so treat DELETED as done. While deletion is still in progress the + // block is returned with MARKED_FOR_DELETION, which falls through as not-yet-done so we keep polling. done = true - case bb.Status.Status == BUILDING_BLOCK_STATUS_FAILED: - err = fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", bb.Metadata.Uuid) + case bb.Status != nil && bb.Status.Status == BuildingBlockStatusFailed: + err = fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", bbUuidOrUnknown(bb)) } return } + +func (c meshBuildingBlockV2Client) TriggerRun(ctx context.Context, bbUuid string) error { + // trigger-run returns an empty 2xx body; use DoAuthorizedRequest[any] to signal no body expected. + // No body is sent, so the backend triggers a normal (non-dry) apply run. + _, err := internal.DoAuthorizedRequest[any]( + ctx, + c.meshObject.HttpClient, + "POST", + c.meshObject.ApiUrl.JoinPath(bbUuid, "trigger-run"), + internal.WithAccept(c.meshObject.MeshObjectMimeType()), + ) + return err +} diff --git a/building_block_v2_test.go b/building_block_v2_test.go index dc221f9..85313b8 100644 --- a/building_block_v2_test.go +++ b/building_block_v2_test.go @@ -4,6 +4,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { @@ -14,16 +17,16 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { wantErr bool }{ { - name: "nil (hard deletion / 404)", + name: "nil (404 — hard deletion / purge)", bb: nil, wantDone: true, wantErr: false, }, { - name: "lifecycle state DELETED", + name: "lifecycle state DELETED (soft delete completed, block still returned)", bb: &MeshBuildingBlockV2{ Status: &MeshBuildingBlockV2Status{ - Lifecycle: MeshBuildingBlockV2Lifecycle{State: BUILDING_BLOCK_LIFECYCLE_STATE_DELETED}, + Lifecycle: MeshBuildingBlockV2Lifecycle{State: BuildingBlockLifecycleStateDeleted}, }, }, wantDone: true, @@ -40,11 +43,22 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { wantDone: false, wantErr: true, }, + { + name: "status FAILED with nil Uuid does not panic", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: nil}, + Status: &MeshBuildingBlockV2Status{ + Status: BuildingBlockStatusFailed, + }, + }, + wantDone: false, + wantErr: true, + }, { name: "still in progress (MARKED_FOR_DELETION lifecycle, non-failed status)", bb: &MeshBuildingBlockV2{ Status: &MeshBuildingBlockV2Status{ - Lifecycle: MeshBuildingBlockV2Lifecycle{State: BUILDING_BLOCK_LIFECYCLE_STATE_MARKED_FOR_DELETION}, + Lifecycle: MeshBuildingBlockV2Lifecycle{State: BuildingBlockLifecycleStateMarkedForDeletion}, }, }, wantDone: false, @@ -64,3 +78,97 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { }) } } + +func TestMeshBuildingBlockV2_CreateSuccessful(t *testing.T) { + tests := []struct { + name string + bb *MeshBuildingBlockV2 + wantDone bool + wantErr bool + errContains string + }{ + { + name: "nil (not found after creation)", + bb: nil, + wantDone: false, + wantErr: true, + }, + { + name: "no status yet — keep polling", + bb: &MeshBuildingBlockV2{Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}}, + wantDone: false, + wantErr: false, + }, + { + name: "SUCCEEDED", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{Status: BuildingBlockStatusSucceeded}, + }, + wantDone: true, + wantErr: false, + }, + { + name: "FAILED", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{Status: BuildingBlockStatusFailed}, + }, + wantDone: false, + wantErr: true, + }, + { + name: "ABORTED", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{Status: BuildingBlockStatusAborted}, + }, + wantDone: false, + wantErr: true, + }, + { + name: "WAITING_FOR_USER_INPUT — terminal but non-fatal", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{Status: BuildingBlockStatusWaitingForUserInput}, + }, + wantDone: true, + wantErr: false, + }, + { + name: "FAILED with nil Uuid does not panic", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: nil}, + Status: &MeshBuildingBlockV2Status{Status: BuildingBlockStatusFailed}, + }, + wantDone: false, + wantErr: true, + errContains: "", + }, + { + name: "unknown status — fail fast", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{Status: enum.Entry[BuildingBlockStatus]("SOMETHING_NEW")}, + }, + wantDone: false, + wantErr: true, + errContains: "unknown building block status", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + done, err := tt.bb.CreateSuccessful() + assert.Equal(t, tt.wantDone, done) + if tt.wantErr { + require.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/client.go b/client.go index 1c3c194..52988c6 100644 --- a/client.go +++ b/client.go @@ -21,6 +21,7 @@ type Client struct { ApiKey MeshApiKeyClient BuildingBlock MeshBuildingBlockClient BuildingBlockV2 MeshBuildingBlockV2Client + BuildingBlockRun MeshBuildingBlockRunClient BuildingBlockDefinition MeshBuildingBlockDefinitionClient BuildingBlockDefinitionVersion MeshBuildingBlockDefinitionVersionClient BuildingBlockRunner MeshBuildingBlockRunnerClient @@ -72,6 +73,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza ApiKey: newApiKeyClient(ctx, httpClient), BuildingBlock: newBuildingBlockClient(ctx, httpClient), BuildingBlockV2: newBuildingBlockV2Client(ctx, httpClient), + BuildingBlockRun: newBuildingBlockRunClient(ctx, httpClient), BuildingBlockDefinition: newBuildingBlockDefinitionClient(ctx, httpClient), BuildingBlockDefinitionVersion: newBuildingBlockDefinitionVersionClient(ctx, httpClient), BuildingBlockRunner: newBuildingBlockRunnerClient(ctx, httpClient), diff --git a/client_kind.go b/client_kind.go index 6bc91fa..3264d46 100644 --- a/client_kind.go +++ b/client_kind.go @@ -4,6 +4,7 @@ package client type meshObjectKind struct { ApiKey string BuildingBlock string + BuildingBlockRun string BuildingBlockDefinition string BuildingBlockDefinitionVersion string BuildingBlockRunner string @@ -28,6 +29,7 @@ type meshObjectKind struct { var MeshObjectKind = meshObjectKind{ ApiKey: "meshApiKey", BuildingBlock: "meshBuildingBlock", + BuildingBlockRun: "meshBuildingBlockRun", BuildingBlockDefinition: "meshBuildingBlockDefinition", BuildingBlockDefinitionVersion: "meshBuildingBlockDefinitionVersion", BuildingBlockRunner: "meshBuildingBlockRunner", diff --git a/internal/http_error.go b/internal/http_error.go index 92030fc..55cc32c 100644 --- a/internal/http_error.go +++ b/internal/http_error.go @@ -25,3 +25,8 @@ func (e HttpError) IsForbidden() bool { func (e HttpError) IsNotFound() bool { return e.StatusCode == http.StatusNotFound } + +// IsConflict returns true if the error is a 409 Conflict response. +func (e HttpError) IsConflict() bool { + return e.StatusCode == http.StatusConflict +} diff --git a/types/clienttypes.go b/types/clienttypes.go index 17589cb..e0ec320 100644 --- a/types/clienttypes.go +++ b/types/clienttypes.go @@ -11,8 +11,10 @@ type ( Set[T any] []T Secret struct { + // Plaintext is optionally set if secret is initially created (or rotated later) Plaintext *string `json:"plaintext,omitempty" tfsdk:"plaintext"` - Hash *string `json:"hash,omitempty" tfsdk:"-"` + // Hash is always present in responses (Plaintext is never returned) and set in requests if secret is supposed to be kept. + Hash *string `json:"hash,omitempty" tfsdk:"-"` } SecretOrAny = variant.Variant[Secret, any] From ed533bfe9653adde49468d38ea495361ade72e95 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 25 Jun 2026 10:50:44 +0200 Subject: [PATCH 155/200] feat: meshstack_building_blocks data source Add a read-only, filterable list data source backed by the v2-preview building block list endpoint. Each entry mirrors the meshstack_building_block resource (metadata/spec/status/all_inputs); sensitive inputs are surfaced as a hash only, reusing the secret data-source schema. Filters: workspace/project/platform identifier, name, definition_uuid, version_uuid, version_number (lenient "v1"/"1"), tenant_uuid, target_kind, status, lifecycle_states (repeated query param; empty => all), and the platform-operator scope selectors managed_by_definition_uuid / managed_by_workspace_identifier (MANAGED_BUILDINGBLOCK_LIST). Adds a List method + filter struct on the building block v2 client, a WithUrlQueryValues option for repeated query params, and a mock List implementation. Reuses the resource's all_inputs mapping via an extracted buildAllInput helper. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_v2.go | 56 ++++++++++++++++++++++++++++++++++++ internal/http_client_test.go | 22 ++++++++++++-- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/building_block_v2.go b/building_block_v2.go index 8e0d494..1c2f2a8 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -136,9 +136,36 @@ type MeshBuildingBlockOutput struct { AssignmentType enum.Entry[MeshBuildingBlockDefinitionOutputAssignmentType] `json:"assignmentType" tfsdk:"assignment_type"` } +// MeshBuildingBlockV2ListFilter holds the optional query filters for listing building blocks +// via the v2-preview list endpoint. All scalar fields are nil when unset (omitted from the +// query). The backend returns only active building blocks; soft-deleted ones are not listed. +type MeshBuildingBlockV2ListFilter struct { + WorkspaceIdentifier *string + ProjectIdentifier *string + PlatformIdentifier *string + Name *string + // DefinitionUuid filters by the owning building block definition's UUID (not a version). + DefinitionUuid *string + // VersionUuid filters by a specific building block definition version UUID. + VersionUuid *string + // VersionNumber filters by the literal definition version number. The backend parses it + // leniently, so both "v1" and "1" match version 1. + VersionNumber *string + TenantUuid *string + // TargetKind filters by target ref kind, one of meshTenant or meshWorkspace. + TargetKind *string + Status *string + // ManagedByWorkspaceIdentifier and ManagedByDefinitionUuid select the platform-operator + // (managed) permission scope: building blocks created from definitions owned by the given + // workspace / definition. Requires the MANAGED_BUILDINGBLOCK_LIST authority. + ManagedByWorkspaceIdentifier *string + ManagedByDefinitionUuid *string +} + type MeshBuildingBlockV2Client interface { Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) + List(ctx context.Context, filter *MeshBuildingBlockV2ListFilter) ([]MeshBuildingBlockV2, error) Create(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) Update(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) Delete(ctx context.Context, uuid string, purge bool) error @@ -163,6 +190,35 @@ func (c meshBuildingBlockV2Client) ReadFunc(uuid string) func(ctx context.Contex } } +func (c meshBuildingBlockV2Client) List(ctx context.Context, filter *MeshBuildingBlockV2ListFilter) ([]MeshBuildingBlockV2, error) { + var options []internal.RequestOption + + // Map each non-nil scalar filter to its query param. Names must match the backend + // fetchBuildingBlocksV2 @RequestParam names exactly; a typo silently disables the filter. + if filter != nil { + for key, value := range map[string]*string{ + "workspaceIdentifier": filter.WorkspaceIdentifier, + "projectIdentifier": filter.ProjectIdentifier, + "platformIdentifier": filter.PlatformIdentifier, + "name": filter.Name, + "definitionUuid": filter.DefinitionUuid, + "versionUuid": filter.VersionUuid, + "versionNumber": filter.VersionNumber, + "tenantUuid": filter.TenantUuid, + "targetRefKind": filter.TargetKind, + "status": filter.Status, + "managedByWorkspaceIdentifier": filter.ManagedByWorkspaceIdentifier, + "managedByDefinitionUuid": filter.ManagedByDefinitionUuid, + } { + if value != nil { + options = append(options, internal.WithUrlQuery(key, *value)) + } + } + } + + return c.meshObject.List(ctx, options...) +} + func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) { return c.meshObject.Post(ctx, bb) } diff --git a/internal/http_client_test.go b/internal/http_client_test.go index ea38a0f..152799d 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -36,7 +36,7 @@ func TestHttpClient(t *testing.T) { t.Run("DoRequest object call with empty 2xx body errors", func(t *testing.T) { client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { - resp.WriteHeader(http.StatusOK) // 200 with no body + resp.WriteHeader(http.StatusOK) }) _, err := DoRequest[*string](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("get")) require.Error(t, err) @@ -45,7 +45,7 @@ func TestHttpClient(t *testing.T) { t.Run("DoRequest no-content call (any) tolerates an empty 2xx body", func(t *testing.T) { client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { - resp.WriteHeader(http.StatusAccepted) // e.g. trigger-run / delete: empty body by design + resp.WriteHeader(http.StatusAccepted) // empty body by design (trigger-run/delete) }) _, err := DoRequest[any](t.Context(), client, http.MethodPost, client.RootUrl.JoinPath("trigger-run")) require.NoError(t, err) @@ -259,6 +259,24 @@ func TestHttpClient(t *testing.T) { }) } +func TestUrlQueryOptions(t *testing.T) { + t.Run("WithUrlQuery sets query parameters", func(t *testing.T) { + var gotQuery url.Values + client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + gotQuery = req.URL.Query() + resp.WriteHeader(http.StatusOK) + _, _ = resp.Write([]byte(`"ok"`)) + }) + _, err := DoRequest[string](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("list"), + WithUrlQuery("definitionUuid", "abc"), + WithUrlQuery("status", "SUCCEEDED"), + ) + require.NoError(t, err) + assert.Equal(t, "abc", gotQuery.Get("definitionUuid")) + assert.Equal(t, "SUCCEEDED", gotQuery.Get("status")) + }) +} + func mockTimeNowAsUTC(t *testing.T) time.Time { t.Helper() now := time.Now().UTC().Truncate(time.Second) From 66d8a17e31a5ac26a3fda1062f90c8dbe896791d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 6 Jul 2026 07:13:36 +0200 Subject: [PATCH 156/200] fix: retry DELETE and widen retry budget for backend restarts Smoke tests against dev repeatedly failed with `503 Service Unavailable` whenever the meshfed backend restarted (e.g. an OOMKill + Spring Boot cold start), which leaves the gateway returning 503 for ~2-3 minutes. Two gaps let those transient 503s surface as hard failures: - DELETE was never retried (only GET/PUT and whitelisted POST /api/login), so a building block delete that hit a 503 failed immediately. DELETE is idempotent, so replaying it is safe. - The retry budget was only ~75s (MaxRetries 10, MaxWait 10s), shorter than a typical backend restart, so even the retried GET paths (BBD read, status polling, /mesh/info version check on provider configure) exhausted retries before the backend came back. Add DELETE to the idempotent methods retried on 429/502/503/504 and transport errors, and widen the budget to ~4 minutes (MaxRetries 12, MaxWait 30s). Co-Authored-By: Claude Opus 4.8 (1M context) --- client.go | 8 ++++++-- internal/http_client_test.go | 15 +++++++++++++++ internal/retry.go | 9 ++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/client.go b/client.go index 52988c6..b366b31 100644 --- a/client.go +++ b/client.go @@ -59,8 +59,12 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza httpClient := internal.WithRetry( internal.NewHttpClient(rootUrl, userAgent, auth), internal.RetryOptions{ - MaxRetries: 10, - Backoff: internal.ExponentialBackoff{MinWait: 1 * time.Second, MaxWait: 10 * time.Second}, + // Sized to ride out a full meshStack backend restart (e.g. an OOMKill followed by a + // Spring Boot cold start), which can leave the gateway returning 503 for ~2-3 minutes — + // well beyond the previous ~75s budget. This backoff sequence sums to ~4 minutes: + // 1+2+4+8+16+30*7 seconds. + MaxRetries: 12, + Backoff: internal.ExponentialBackoff{MinWait: 1 * time.Second, MaxWait: 30 * time.Second}, WhitelistedPaths: map[string][]string{"POST": {apiLoginPath}}, }, ) diff --git a/internal/http_client_test.go b/internal/http_client_test.go index 152799d..36da3a2 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -129,6 +129,21 @@ func TestHttpClient(t *testing.T) { assert.Equal(t, 1, attempts, "PATCH must not be retried") }) + t.Run("DoRequest with DELETE (retried, idempotent)", func(t *testing.T) { + attempts := 0 + client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { + attempts++ + if attempts == 1 { + resp.WriteHeader(503) + return + } + resp.WriteHeader(http.StatusNoContent) + }), RetryOptions{MaxRetries: 3, Backoff: &retryTestBackoff{}}) + _, err := DoRequest[any](t.Context(), client, http.MethodDelete, client.RootUrl.JoinPath("delete")) + require.NoError(t, err) + assert.Equal(t, 2, attempts, "DELETE must be retried after a 503") + }) + t.Run("DoRequest with PUT replays body on retry", func(t *testing.T) { attempt := 0 client := WithRetry(newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { diff --git a/internal/retry.go b/internal/retry.go index 6906422..5d38278 100644 --- a/internal/retry.go +++ b/internal/retry.go @@ -14,8 +14,8 @@ import ( ) // WithRetry sets up the given client to retry certain requests. -// GET and PUT are retried by default, POST only if the path is explicitly whitelisted. -// See RetryOptions. +// The idempotent methods GET, PUT and DELETE are retried by default, POST only if the path is +// explicitly whitelisted. See RetryOptions. func WithRetry(c HttpClient, options RetryOptions) HttpClient { next := http.DefaultTransport if c.Transport != nil { @@ -40,7 +40,10 @@ func WithRetry(c HttpClient, options RetryOptions) HttpClient { return false } switch req.Method { - case http.MethodGet, http.MethodPut: + case http.MethodGet, http.MethodPut, http.MethodDelete: + // Idempotent methods are safe to retry: replaying them cannot create duplicate + // side effects. A DELETE that actually succeeded server-side before a proxy 503 + // simply yields a 404 on replay, which delete handlers already treat as done. return true } if whitelisted, found := whitelistedByMethodAndUrl[req.Method]; found { From b244bace5dbecc5cdbe19117e46f5340fcd740ef Mon Sep 17 00:00:00 2001 From: Stefan Tomm Date: Tue, 7 Jul 2026 11:33:00 +0200 Subject: [PATCH 157/200] fix: prepare for upcoming WAITING_FOR_APPROVAL building block status meshStack will soon add an approval gate that surfaces building block runs as WAITING_FOR_APPROVAL (added in meshfed, not released yet). The provider's await logic treats any status not in its BuildingBlockStatuses enum as fatal ("unknown building block status; provider may be out of date"), so once the backend starts returning it an awaited create/update would error when a run parked for approval. Add WAITING_FOR_APPROVAL to the enum and to IsWaitingForInput() ahead of that rollout so it is treated as a non-terminal, non-fatal parked state: polling stops and a "waiting for input" warning is surfaced, matching the other WAITING_FOR_* states. Generalize the warning wording to cover approvals, complete the legacy v1 status doc strings, and regenerate docs. Co-Authored-By: Claude Opus 4.8 --- building_block_v2.go | 9 ++++++--- building_block_v2_test.go | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/building_block_v2.go b/building_block_v2.go index 1c2f2a8..31eca84 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -28,6 +28,7 @@ var ( BuildingBlockStatusWaitingForDependentInput = BuildingBlockStatuses.Entry("WAITING_FOR_DEPENDENT_INPUT") BuildingBlockStatusWaitingForOperatorInput = BuildingBlockStatuses.Entry("WAITING_FOR_OPERATOR_INPUT") BuildingBlockStatusWaitingForUserInput = BuildingBlockStatuses.Entry("WAITING_FOR_USER_INPUT") + BuildingBlockStatusWaitingForApproval = BuildingBlockStatuses.Entry("WAITING_FOR_APPROVAL") BuildingBlockStatusPending = BuildingBlockStatuses.Entry("PENDING") BuildingBlockStatusInProgress = BuildingBlockStatuses.Entry("IN_PROGRESS") BuildingBlockStatusSucceeded = BuildingBlockStatuses.Entry("SUCCEEDED") @@ -239,12 +240,14 @@ func (c meshBuildingBlockV2Client) Delete(ctx context.Context, uuid string, purg } // IsWaitingForInput reports whether the building block run is paused awaiting -// human or dependency input. Such a run will not progress on its own, so polling -// callers treat it as a terminal (but non-fatal) state and surface a warning. +// human input, a dependency, or an approval. Such a run will not progress on its +// own, so polling callers treat it as a terminal (but non-fatal) state and surface +// a warning. func (bb *MeshBuildingBlockV2) IsWaitingForInput() bool { return bb.Status.Status == BuildingBlockStatusWaitingForOperatorInput || bb.Status.Status == BuildingBlockStatusWaitingForUserInput || - bb.Status.Status == BuildingBlockStatusWaitingForDependentInput + bb.Status.Status == BuildingBlockStatusWaitingForDependentInput || + bb.Status.Status == BuildingBlockStatusWaitingForApproval } // bbUuidOrUnknown returns the building block UUID for diagnostic messages, or "" if nil. diff --git a/building_block_v2_test.go b/building_block_v2_test.go index 85313b8..d93d6a9 100644 --- a/building_block_v2_test.go +++ b/building_block_v2_test.go @@ -135,6 +135,15 @@ func TestMeshBuildingBlockV2_CreateSuccessful(t *testing.T) { wantDone: true, wantErr: false, }, + { + name: "WAITING_FOR_APPROVAL — terminal but non-fatal", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{Status: BuildingBlockStatusWaitingForApproval}, + }, + wantDone: true, + wantErr: false, + }, { name: "FAILED with nil Uuid does not panic", bb: &MeshBuildingBlockV2{ From 1c138e69318254315fde3f17f48d3e38c5bd5063 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Fri, 3 Jul 2026 21:28:32 +0200 Subject: [PATCH 158/200] feat: allow to manage display_order value for building block definition I/O without affecting the calculated hash for change detection CU-86cabn76y --- building_block_definition_version.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index 72c38aa..7882d1c 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -112,6 +112,7 @@ type MeshBuildingBlockDefinitionInput struct { Description *string `json:"description,omitempty" tfsdk:"description"` ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` + DisplayOrder int64 `json:"displayOrder,omitempty" tfsdk:"display_order"` } func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSON(bytes []byte) error { @@ -148,6 +149,7 @@ type MeshBuildingBlockDefinitionOutput struct { DisplayName string `json:"displayName" tfsdk:"display_name"` Type MeshBuildingBlockIOType `json:"type" tfsdk:"type"` AssignmentType MeshBuildingBlockDefinitionOutputAssignmentType `json:"assignmentType" tfsdk:"assignment_type"` + DisplayOrder int64 `json:"displayOrder,omitempty" tfsdk:"display_order"` } // Main version types From 37932cf07ae3715ad1136f5208e8517777e22395 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 9 Jul 2026 22:21:56 +0200 Subject: [PATCH 159/200] fix(building_block_definition): send display_order 0 so it round-trips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit display_order was serialized with json `omitempty`, so the schema default of 0 (and what an unknown plan value collapses to via WithSetUnknownValueToZero) was dropped from the request. The backend then assigned a position itself, so the applied value differed from the plan — "Provider produced inconsistent result after apply: ...display_order: was 0, now 1" across the acceptance suite. - client: drop `omitempty` on input/output DisplayOrder (value int64) so 0 is sent and the backend stores it verbatim. - content hash: normalize display_order to 0 before hashing so presentation-only reordering is not a content change and the hash stays decoupled from the wire value. Golden hashes updated accordingly (the invariants still hold). - test: 07_manual_computed_outputs expects the backend's positional display_order for derived outputs (input order, 0-based: approval=0, region=1, ticket=2), per meshfed ManualDefinitionVersionService. - mock: derive manual output display_order by input index to mirror the backend. Verified against a local develop backend: TestAccBuildingBlockDefinition (all subtests) and TestAccLandingZone pass; unit tests and lint are green. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_definition_version.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index 7882d1c..5e88a93 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -112,7 +112,10 @@ type MeshBuildingBlockDefinitionInput struct { Description *string `json:"description,omitempty" tfsdk:"description"` ValueValidationRegex *string `json:"valueValidationRegex,omitempty" tfsdk:"value_validation_regex"` ValidationRegexErrorMessage *string `json:"validationRegexErrorMessage,omitempty" tfsdk:"validation_regex_error_message"` - DisplayOrder int64 `json:"displayOrder,omitempty" tfsdk:"display_order"` + // No omitempty: a 0 (the schema default, and what an unknown plan value collapses to) must be sent so + // the backend stores it verbatim. With omitempty the 0 would be dropped and the backend would assign + // a position itself, making the applied value differ from the plan. + DisplayOrder int64 `json:"displayOrder" tfsdk:"display_order"` } func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSON(bytes []byte) error { @@ -149,7 +152,8 @@ type MeshBuildingBlockDefinitionOutput struct { DisplayName string `json:"displayName" tfsdk:"display_name"` Type MeshBuildingBlockIOType `json:"type" tfsdk:"type"` AssignmentType MeshBuildingBlockDefinitionOutputAssignmentType `json:"assignmentType" tfsdk:"assignment_type"` - DisplayOrder int64 `json:"displayOrder,omitempty" tfsdk:"display_order"` + // No omitempty so a 0 is sent, not dropped (see MeshBuildingBlockDefinitionInput.DisplayOrder). + DisplayOrder int64 `json:"displayOrder" tfsdk:"display_order"` } // Main version types From 3fda00f1199124de49565d417f142e50b4ad79ad Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 14 Jul 2026 13:49:38 +0200 Subject: [PATCH 160/200] fix(building_block): tolerate transient FAILED during a force-purge deletion When a building block's definition uses deletion_mode = PURGE, the backend force-purges the block: it soft-deletes it regardless of the delete run's outcome. The delete run still executes, so a block whose destroy run fails passes through a transient FAILED status before the lifecycle reaches DELETED. DeletionSuccessful treated any FAILED status as terminal, so a delete-poll that happened to sample that transient window aborted with "reached FAILED state during deletion" even though the block was about to be purged -- an intermittent, timing-dependent deletion failure. Tolerate FAILED while status.forcePurge is set and keep polling until DELETED. A FAILED deletion that is not being purged is still surfaced as an error. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_v2.go | 8 +++++++- building_block_v2_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/building_block_v2.go b/building_block_v2.go index 31eca84..79e5e89 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -291,7 +291,13 @@ func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) { // block is returned with MARKED_FOR_DELETION, which falls through as not-yet-done so we keep polling. done = true case bb.Status != nil && bb.Status.Status == BuildingBlockStatusFailed: - err = fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", bbUuidOrUnknown(bb)) + // A force-purge (definition deletion_mode = PURGE, or an admin purge) deletes the block + // regardless of its delete run's outcome, so a FAILED status here is transient — the + // lifecycle still proceeds to DELETED. Keep polling instead of erroring on that transient + // FAILED. Only a FAILED delete that is NOT being force-purged is a genuine stuck deletion. + if !bb.Status.ForcePurge { + err = fmt.Errorf("building block %s reached FAILED state during deletion. For more details, check the building block run logs in meshStack", bbUuidOrUnknown(bb)) + } } return } diff --git a/building_block_v2_test.go b/building_block_v2_test.go index d93d6a9..d87f6cf 100644 --- a/building_block_v2_test.go +++ b/building_block_v2_test.go @@ -43,6 +43,18 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { wantDone: false, wantErr: true, }, + { + name: "status FAILED but force-purged keeps polling (transient, will reach DELETED)", + bb: &MeshBuildingBlockV2{ + Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, + Status: &MeshBuildingBlockV2Status{ + Status: BuildingBlockStatusFailed, + ForcePurge: true, + }, + }, + wantDone: false, + wantErr: false, + }, { name: "status FAILED with nil Uuid does not panic", bb: &MeshBuildingBlockV2{ From 2eff7cf53bf93521c5dec7b771e2bc19b405f85a Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 16 Jul 2026 14:17:51 +0200 Subject: [PATCH 161/200] feat: add meshTenant UUID as BB input assignmentType --- building_block_definition_version.go | 1 + 1 file changed, 1 insertion(+) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index 5e88a93..bb1d0e4 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -59,6 +59,7 @@ var ( MeshBuildingBlockInputAssignmentTypePlatformOperatorManualInput = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_OPERATOR_MANUAL_INPUT") MeshBuildingBlockInputAssignmentTypeBuildingBlockOutput = MeshBuildingBlockInputAssignmentTypes.Entry("BUILDING_BLOCK_OUTPUT") MeshBuildingBlockInputAssignmentTypePlatformTenantID = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_TENANT_ID") + MeshBuildingBlockInputAssignmentTypeMeshTenantUuid = MeshBuildingBlockInputAssignmentTypes.Entry("MESH_TENANT_UUID") MeshBuildingBlockInputAssignmentTypeWorkspaceIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("WORKSPACE_IDENTIFIER") MeshBuildingBlockInputAssignmentTypeProjectIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("PROJECT_IDENTIFIER") MeshBuildingBlockInputAssignmentTypeFullPlatformIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("FULL_PLATFORM_IDENTIFIER") From 38137db95d9c0caa9bc84bba1e459391ec4a2a28 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 16 Jul 2026 15:38:14 +0200 Subject: [PATCH 162/200] refactor: rename MESH_TENANT_UUID to MESHSTACK_TENANT_ID --- building_block_definition_version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index bb1d0e4..f5e845c 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -59,7 +59,7 @@ var ( MeshBuildingBlockInputAssignmentTypePlatformOperatorManualInput = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_OPERATOR_MANUAL_INPUT") MeshBuildingBlockInputAssignmentTypeBuildingBlockOutput = MeshBuildingBlockInputAssignmentTypes.Entry("BUILDING_BLOCK_OUTPUT") MeshBuildingBlockInputAssignmentTypePlatformTenantID = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_TENANT_ID") - MeshBuildingBlockInputAssignmentTypeMeshTenantUuid = MeshBuildingBlockInputAssignmentTypes.Entry("MESH_TENANT_UUID") + MeshBuildingBlockInputAssignmentTypeMeshstackTenantId = MeshBuildingBlockInputAssignmentTypes.Entry("MESHSTACK_TENANT_ID") MeshBuildingBlockInputAssignmentTypeWorkspaceIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("WORKSPACE_IDENTIFIER") MeshBuildingBlockInputAssignmentTypeProjectIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("PROJECT_IDENTIFIER") MeshBuildingBlockInputAssignmentTypeFullPlatformIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("FULL_PLATFORM_IDENTIFIER") From f7313c9a1071f4eb84fd77bb96acb13aadc0e856 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 17 Jul 2026 10:15:40 +0200 Subject: [PATCH 163/200] refactor: rename MESHSTACK_TENANT_ID assignment type to MESHSTACK_TENANT_UUID (#238) * refactor: rename MESHSTACK_TENANT_ID assignment type to MESHSTACK_TENANT_UUID The value assigned is the meshTenant's UUID, so name the assignment type accordingly. The old name was never in a tagged release (pending v0.23.3), so this only affects pre-release users; noted in CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(changelog): clarify MESHSTACK_TENANT_UUID rename note Co-Authored-By: Claude Opus 4.8 (1M context) * docs(changelog): drop MESHSTACK_TENANT_ID note; feature entry names the new type directly Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- building_block_definition_version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index f5e845c..60d5751 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -59,7 +59,7 @@ var ( MeshBuildingBlockInputAssignmentTypePlatformOperatorManualInput = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_OPERATOR_MANUAL_INPUT") MeshBuildingBlockInputAssignmentTypeBuildingBlockOutput = MeshBuildingBlockInputAssignmentTypes.Entry("BUILDING_BLOCK_OUTPUT") MeshBuildingBlockInputAssignmentTypePlatformTenantID = MeshBuildingBlockInputAssignmentTypes.Entry("PLATFORM_TENANT_ID") - MeshBuildingBlockInputAssignmentTypeMeshstackTenantId = MeshBuildingBlockInputAssignmentTypes.Entry("MESHSTACK_TENANT_ID") + MeshBuildingBlockInputAssignmentTypeMeshstackTenantUuid = MeshBuildingBlockInputAssignmentTypes.Entry("MESHSTACK_TENANT_UUID") MeshBuildingBlockInputAssignmentTypeWorkspaceIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("WORKSPACE_IDENTIFIER") MeshBuildingBlockInputAssignmentTypeProjectIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("PROJECT_IDENTIFIER") MeshBuildingBlockInputAssignmentTypeFullPlatformIdentifier = MeshBuildingBlockInputAssignmentTypes.Entry("FULL_PLATFORM_IDENTIFIER") From 868021d45ebcc8f20ebc6ce50fb5d2a6b170f2b7 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Mon, 20 Jul 2026 08:15:21 +0200 Subject: [PATCH 164/200] feat: allow all dedicated (not NONE) assignment types for outputs on manual BB definition version inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual building blocks (implementation.manual) now accept any special output assignment_type — SIGN_IN_URL, RESOURCE_URL, and SUMMARY in addition to PLATFORM_TENANT_ID — to mark how a derived output is used. Previously only PLATFORM_TENANT_ID was allowed. The output key must match an input key; the backend still derives the output set from the inputs (see #131, #176). The non-NONE set is derived once from the full enum via a new enum.Enum.Except helper (nonNoneOutputAssignmentTypes), so adding an assignment type flows into the schema description, ValidateConfig, and the tests without editing each site. Co-Authored-By: Claude Opus 4.8 (1M context) --- types/enum/enum.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/types/enum/enum.go b/types/enum/enum.go index 0f07fef..a8279e6 100644 --- a/types/enum/enum.go +++ b/types/enum/enum.go @@ -2,6 +2,7 @@ package enum import ( "fmt" + "slices" "strings" ) @@ -28,6 +29,14 @@ func (e Enum[T]) Strings() []string { return e.to(Entry[T].String) } +// Except returns the enum minus the given entries, preserving order. Deriving a subset this way keeps a +// single source of truth: adding an entry to the base enum flows into the subset automatically. +func (e Enum[T]) Except(excluded ...Entry[T]) Enum[T] { + return slices.DeleteFunc(slices.Clone(e), func(ee Entry[T]) bool { + return slices.Contains(excluded, ee) + }) +} + func (e Enum[T]) Markdown() string { return strings.Join(e.to(Entry[T].Markdown), ", ") } From fb1aa92c6c1ae8dfd714716d56b6adeda20e8d7a Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 16 Jul 2026 22:20:43 +0200 Subject: [PATCH 165/200] refactor: consolidate meshObject reference handling behind a single meshRef helper Collapse the near-duplicate reference-schema helpers into one builder so every meshObject reference is constructed the same way. - `meshRefByUuid` / `meshRefByName` constructors take a `meshRefOptions` struct with two behaviour flags (`Output`, `OptionalComputed`); the zero value is a required input. This replaces the earlier `meshRef(kind, refId, desc, ...opts)` signature and the `asOutput()` / `required()` / `optionalComputed()` / `requiredId()` functional options. - Input identifiers stay Optional+Computed with an `AlsoRequires` guard rather than Required: a ref used as a set element collapses to a wholly-unknown element at plan (sets hash by whole value), which a Required nested attribute rejects with "Missing Configuration for Required Attribute". The guard enforces presence while tolerating the unknown; identifiers may also be resolved after apply (computed `.ref`, random suffix). - `refOutputKind` plan modifier keeps an output ref's `kind` known at plan time (it is always the single constant value); only the identifier is computed. - Give `building_block_definition_version_ref` a computed `kind` across all schema sites and the definition's version outputs, so the last outlier ref carries a kind like every other meshObject reference. Docs regenerated; CHANGELOG updated under the pending v0.23.3. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_v2.go | 1 + 1 file changed, 1 insertion(+) diff --git a/building_block_v2.go b/building_block_v2.go index 79e5e89..868ad5c 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -101,6 +101,7 @@ func (m *MeshBuildingBlockInput) UnmarshalJSON(bytes []byte) error { type MeshBuildingBlockV2DefinitionVersionRef struct { Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind" tfsdk:"kind"` // ContentHash is a Terraform-only field (json:"-", never sent to or returned by the backend). // It lets a config signal that the referenced version's content changed so a rerun is triggered // even though the version uuid is unchanged. The building_block (v3) resource honors it via the From 05780df40073b67fbf882c8b6b6b770326a854ff Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 17 Jul 2026 08:28:00 +0200 Subject: [PATCH 166/200] refactor: consolidate client ref DTOs into shared NamedRef/UuidRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the provider-side meshRef consolidation on the client side. Add client/refs.go with two DTO structs — NamedRef ({name, kind}) and UuidRef ({uuid, kind}) — as the counterparts of the meshRefByName / meshRefByUuid schema builders, and route every {name|uuid, kind} reference through them. - Remove the near-duplicate named types (MeshProjectRoleRefV2, PlatformTypeRef, LocationRef, BuildingBlockDefinitionSupportedPlatform → NamedRef; BuildingBlockRunnerRef, MeshBuildingBlockDefinitionRef, MeshLandingZonePlatformRef, MeshIntegrationRef, BuildingBlockDefinitionRef → UuidRef). - A ref that adds fields embeds the matching struct by value: MeshBuildingBlockV2DefinitionVersionRef now embeds UuidRef next to its content_hash; json and tfsdk reflection both promote the embedded fields. - MeshBuildingBlockV2TargetRef (mixes uuid and name) stays bespoke. Wire format and tfsdk schemas are unchanged — tags are identical and the version ref's content_hash stays tfsdk:"-" — so there is no user-facing change and no CHANGELOG entry. AGENTS.md's meshObject-references section now points at client/refs.go so the client alignment is documented alongside the schema rule. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_definition.go | 11 ++----- building_block_definition_version.go | 16 ++-------- ...block_definition_version_implementation.go | 32 +++++++++---------- building_block_runner.go | 5 --- building_block_v2.go | 3 +- buildingblock.go | 5 --- integration_config.go | 22 ++++++------- landingzone.go | 11 ++----- platform.go | 7 +--- platform_config_aws.go | 12 +++---- platform_config_azure.go | 4 +-- platform_config_custom.go | 7 +--- platform_config_gcp.go | 4 +-- platform_config_openshift.go | 4 +-- platform_properties_aks.go | 4 +-- platform_properties_aws.go | 6 ++-- platform_properties_azure.go | 2 +- platform_properties_azurerg.go | 6 ++-- platform_properties_gcp.go | 4 +-- project_binding.go | 7 +--- refs.go | 19 +++++++++++ 21 files changed, 81 insertions(+), 110 deletions(-) create mode 100644 refs.go diff --git a/building_block_definition.go b/building_block_definition.go index 5259cc1..46f062a 100644 --- a/building_block_definition.go +++ b/building_block_definition.go @@ -22,11 +22,6 @@ type MeshBuildingBlockDefinitionMetadata struct { Tags map[string][]string `json:"tags" tfsdk:"tags"` } -type BuildingBlockDefinitionSupportedPlatform struct { - Kind string `json:"kind" tfsdk:"kind"` - Name string `json:"name" tfsdk:"name"` -} - type MeshBuildingBlockDefinitionSpec struct { DisplayName string `json:"displayName" tfsdk:"display_name"` TargetType MeshBuildingBlockType `json:"targetType" tfsdk:"target_type"` @@ -37,9 +32,9 @@ type MeshBuildingBlockDefinitionSpec struct { SupportURL *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationURL *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` // NotificationSubscribers can also specify emails with prefix 'email:', so it's not only usernames (as the JSON field name suggests)! - NotificationSubscribers types.Set[string] `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` - Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` - SupportedPlatforms types.Set[BuildingBlockDefinitionSupportedPlatform] `json:"supportedPlatforms" tfsdk:"supported_platforms"` + NotificationSubscribers types.Set[string] `json:"notificationSubscriberUsernames,omitempty" tfsdk:"notification_subscribers"` + Symbol *string `json:"symbol,omitempty" tfsdk:"symbol"` + SupportedPlatforms types.Set[NamedRef] `json:"supportedPlatforms" tfsdk:"supported_platforms"` } type MeshBuildingBlockDefinitionStatusVersion struct { diff --git a/building_block_definition_version.go b/building_block_definition_version.go index 60d5751..7212dde 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -79,18 +79,6 @@ var ( MeshBuildingBlockDefinitionOutputAssignmentTypeSummary = MeshBuildingBlockDefinitionOutputAssignmentTypes.Entry("SUMMARY") ) -// Ref types - -type BuildingBlockDefinitionRef struct { - Uuid string `json:"uuid"` - Kind string `json:"kind"` -} - -type MeshIntegrationRef struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - Kind string `json:"kind" tfsdk:"kind"` -} - // Input and Output types type MeshBuildingBlockDefinitionInput struct { @@ -167,14 +155,14 @@ type MeshBuildingBlockDefinitionVersionMetadata struct { type BuildingBlockDependencyRef string type MeshBuildingBlockDefinitionVersionSpec struct { - BuildingBlockDefinitionRef *BuildingBlockDefinitionRef `json:"buildingBlockDefinitionRef" tfsdk:"-"` + BuildingBlockDefinitionRef *UuidRef `json:"buildingBlockDefinitionRef" tfsdk:"-"` OnlyApplyOncePerTenant bool `json:"onlyApplyOncePerTenant" tfsdk:"only_apply_once_per_tenant"` DeletionMode BuildingBlockDeletionMode `json:"deletionMode" tfsdk:"deletion_mode"` Permissions types.Set[ApiPermission] `json:"permissions,omitempty" tfsdk:"permissions"` Outputs map[string]MeshBuildingBlockDefinitionOutput `json:"outputs" tfsdk:"outputs"` VersionNumber *int64 `json:"versionNumber,omitempty" tfsdk:"version_number"` State *MeshBuildingBlockDefinitionVersionState `json:"state,omitempty" tfsdk:"state"` - RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + RunnerRef *UuidRef `json:"runnerRef" tfsdk:"runner_ref"` DependencyDefinitionUUIDs types.Set[BuildingBlockDependencyRef] `json:"dependencyDefinitionUuids,omitempty" tfsdk:"dependency_refs"` Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` diff --git a/building_block_definition_version_implementation.go b/building_block_definition_version_implementation.go index c16c397..d8ae2dc 100644 --- a/building_block_definition_version_implementation.go +++ b/building_block_definition_version_implementation.go @@ -39,31 +39,31 @@ type MeshBuildingBlockDefinitionTerraformImplementation struct { } type MeshBuildingBlockDefinitionGitHubWorkflowsImplementation struct { - Repository string `json:"repository" tfsdk:"repository"` - Branch string `json:"branch" tfsdk:"branch"` - ApplyWorkflow string `json:"applyWorkflow" tfsdk:"apply_workflow"` - DestroyWorkflow *string `json:"destroyWorkflow" tfsdk:"destroy_workflow"` - Async bool `json:"async" tfsdk:"async"` - OmitRunObjectInput bool `json:"omitRunObjectInput" tfsdk:"omit_run_object_input"` - IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` + Repository string `json:"repository" tfsdk:"repository"` + Branch string `json:"branch" tfsdk:"branch"` + ApplyWorkflow string `json:"applyWorkflow" tfsdk:"apply_workflow"` + DestroyWorkflow *string `json:"destroyWorkflow" tfsdk:"destroy_workflow"` + Async bool `json:"async" tfsdk:"async"` + OmitRunObjectInput bool `json:"omitRunObjectInput" tfsdk:"omit_run_object_input"` + IntegrationRef UuidRef `json:"integrationRef" tfsdk:"integration_ref"` } type MeshBuildingBlockDefinitionManualImplementation struct { } type MeshBuildingBlockDefinitionGitLabPipelineImplementation struct { - ProjectID string `json:"projectId" tfsdk:"project_id"` - RefName string `json:"refName" tfsdk:"ref_name"` - IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` - PipelineTriggerToken types.Secret `json:"pipelineTriggerToken" tfsdk:"pipeline_trigger_token"` + ProjectID string `json:"projectId" tfsdk:"project_id"` + RefName string `json:"refName" tfsdk:"ref_name"` + IntegrationRef UuidRef `json:"integrationRef" tfsdk:"integration_ref"` + PipelineTriggerToken types.Secret `json:"pipelineTriggerToken" tfsdk:"pipeline_trigger_token"` } type MeshBuildingBlockDefinitionAzureDevOpsPipelineImplementation struct { - Project string `json:"project" tfsdk:"project"` - PipelineID string `json:"pipelineId" tfsdk:"pipeline_id"` - RefName *string `json:"refName,omitempty" tfsdk:"ref_name"` - Async bool `json:"async" tfsdk:"async"` - IntegrationRef MeshIntegrationRef `json:"integrationRef" tfsdk:"integration_ref"` + Project string `json:"project" tfsdk:"project"` + PipelineID string `json:"pipelineId" tfsdk:"pipeline_id"` + RefName *string `json:"refName,omitempty" tfsdk:"ref_name"` + Async bool `json:"async" tfsdk:"async"` + IntegrationRef UuidRef `json:"integrationRef" tfsdk:"integration_ref"` } type MeshBuildingBlockDefinitionImplementation struct { diff --git a/building_block_runner.go b/building_block_runner.go index ba63140..8376af3 100644 --- a/building_block_runner.go +++ b/building_block_runner.go @@ -27,11 +27,6 @@ var MeshBuildingBlockRunnerImplementationTypes = []string{ string(MeshBuildingBlockRunnerImplementationTypeAll), } -type BuildingBlockRunnerRef struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - Kind string `json:"kind" tfsdk:"kind"` -} - type MeshBuildingBlockRunner struct { Metadata MeshBuildingBlockRunnerMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshBuildingBlockRunnerSpec `json:"spec" tfsdk:"spec"` diff --git a/building_block_v2.go b/building_block_v2.go index 868ad5c..0f4c612 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -100,8 +100,7 @@ func (m *MeshBuildingBlockInput) UnmarshalJSON(bytes []byte) error { } type MeshBuildingBlockV2DefinitionVersionRef struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - Kind string `json:"kind" tfsdk:"kind"` + UuidRef // ContentHash is a Terraform-only field (json:"-", never sent to or returned by the backend). // It lets a config signal that the referenced version's content changed so a rerun is triggered // even though the version uuid is unchanged. The building_block (v3) resource honors it via the diff --git a/buildingblock.go b/buildingblock.go index fa93b54..8ec9e5d 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -67,11 +67,6 @@ type MeshBuildingBlockCreateMetadata struct { TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` } -type MeshBuildingBlockDefinitionRef struct { - Kind string `json:"kind" tfsdk:"kind"` - Uuid string `json:"uuid" tfsdk:"uuid"` -} - type MeshBuildingBlockClient interface { Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) diff --git a/integration_config.go b/integration_config.go index e88677f..5e23cda 100644 --- a/integration_config.go +++ b/integration_config.go @@ -20,23 +20,23 @@ var ( ) type MeshIntegrationGithubConfig struct { - Owner string `json:"owner" tfsdk:"owner"` - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - AppId string `json:"appId" tfsdk:"app_id"` - AppPrivateKey types.Secret `json:"appPrivateKey" tfsdk:"app_private_key"` - RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + Owner string `json:"owner" tfsdk:"owner"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + AppId string `json:"appId" tfsdk:"app_id"` + AppPrivateKey types.Secret `json:"appPrivateKey" tfsdk:"app_private_key"` + RunnerRef *UuidRef `json:"runnerRef" tfsdk:"runner_ref"` } type MeshIntegrationGitlabConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + RunnerRef *UuidRef `json:"runnerRef" tfsdk:"runner_ref"` } type MeshIntegrationAzureDevopsConfig struct { - BaseUrl string `json:"baseUrl" tfsdk:"base_url"` - Organization string `json:"organization" tfsdk:"organization"` - PersonalAccessToken types.Secret `json:"personalAccessToken" tfsdk:"personal_access_token"` - RunnerRef *BuildingBlockRunnerRef `json:"runnerRef" tfsdk:"runner_ref"` + BaseUrl string `json:"baseUrl" tfsdk:"base_url"` + Organization string `json:"organization" tfsdk:"organization"` + PersonalAccessToken types.Secret `json:"personalAccessToken" tfsdk:"personal_access_token"` + RunnerRef *UuidRef `json:"runnerRef" tfsdk:"runner_ref"` } type MeshIntegrationEntraIdConfig struct { diff --git a/landingzone.go b/landingzone.go index bf7ac67..42474aa 100644 --- a/landingzone.go +++ b/landingzone.go @@ -24,11 +24,11 @@ type MeshLandingZoneSpec struct { AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` - PlatformRef MeshLandingZonePlatformRef `json:"platformRef" tfsdk:"platform_ref"` + PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` PlatformProperties *MeshLandingZonePlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` Quotas []MeshLandingZoneQuota `json:"quotas" tfsdk:"quotas"` - MandatoryBuildingBlockRefs []MeshBuildingBlockDefinitionRef `json:"mandatoryBuildingBlockRefs" tfsdk:"mandatory_building_block_refs"` - RecommendedBuildingBlockRefs []MeshBuildingBlockDefinitionRef `json:"recommendedBuildingBlockRefs" tfsdk:"recommended_building_block_refs"` + MandatoryBuildingBlockRefs []UuidRef `json:"mandatoryBuildingBlockRefs" tfsdk:"mandatory_building_block_refs"` + RecommendedBuildingBlockRefs []UuidRef `json:"recommendedBuildingBlockRefs" tfsdk:"recommended_building_block_refs"` } type MeshLandingZoneStatus struct { @@ -36,11 +36,6 @@ type MeshLandingZoneStatus struct { Restricted bool `json:"restricted" tfsdk:"restricted"` } -type MeshLandingZonePlatformRef struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - Kind string `json:"kind" tfsdk:"kind"` -} - type MeshLandingZonePlatformProperties struct { Type string `json:"type" tfsdk:"type"` Aws *AwsPlatformProperties `json:"aws" tfsdk:"aws"` diff --git a/platform.go b/platform.go index 5de34b3..d49d4d3 100644 --- a/platform.go +++ b/platform.go @@ -25,7 +25,7 @@ type MeshPlatformSpec struct { SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` AccessInformation *string `json:"accessInformation,omitempty" tfsdk:"access_information"` - LocationRef LocationRef `json:"locationRef" tfsdk:"location_ref"` + LocationRef NamedRef `json:"locationRef" tfsdk:"location_ref"` ContributingWorkspaces types.Set[string] `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` Availability PlatformAvailability `json:"availability" tfsdk:"availability"` Config PlatformConfig `json:"config" tfsdk:"config"` @@ -42,11 +42,6 @@ type QuotaDefinition struct { Label string `json:"label" tfsdk:"label"` } -type LocationRef struct { - Kind string `json:"kind" tfsdk:"kind"` - Name string `json:"name" tfsdk:"name"` -} - type PlatformAvailability struct { Restriction string `json:"restriction" tfsdk:"restriction"` PublicationState string `json:"publicationState" tfsdk:"publication_state"` diff --git a/platform_config_aws.go b/platform_config_aws.go index b7a60db..a45a1d2 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -57,9 +57,9 @@ type AwsSsoConfig struct { } type AwsSsoRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AwsRole string `json:"awsRole" tfsdk:"aws_role"` - PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + AwsRole string `json:"awsRole" tfsdk:"aws_role"` + PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` } type AwsEnrollmentConfiguration struct { @@ -76,9 +76,9 @@ type AwsIdentityStoreConfig struct { } type AwsIdentityStoreRoleMapping struct { - ProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AwsRole string `json:"awsRole" tfsdk:"aws_role"` - PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` + ProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + AwsRole string `json:"awsRole" tfsdk:"aws_role"` + PermissionSetArns []string `json:"permissionSetArns" tfsdk:"permission_set_arns"` } type AwsMeteringConfig struct { diff --git a/platform_config_azure.go b/platform_config_azure.go index c753b16..b5d68aa 100644 --- a/platform_config_azure.go +++ b/platform_config_azure.go @@ -71,8 +71,8 @@ type AzureInviteB2BUserConfig struct { } type AzureRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AzureRole AzureRole `json:"azureRole" tfsdk:"azure_role"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureRole AzureRole `json:"azureRole" tfsdk:"azure_role"` } type AzureRole struct { diff --git a/platform_config_custom.go b/platform_config_custom.go index 293cc39..03a632d 100644 --- a/platform_config_custom.go +++ b/platform_config_custom.go @@ -1,15 +1,10 @@ package client type CustomPlatformConfig struct { - PlatformTypeRef PlatformTypeRef `json:"platformTypeRef" tfsdk:"platform_type_ref"` + PlatformTypeRef NamedRef `json:"platformTypeRef" tfsdk:"platform_type_ref"` Metering *CustomMeteringConfig `json:"metering,omitempty" tfsdk:"metering"` } -type PlatformTypeRef struct { - Name string `json:"name" tfsdk:"name"` - Kind string `json:"kind" tfsdk:"kind"` -} - type CustomMeteringConfig struct { Processing *MeshPlatformMeteringProcessingConfig `json:"processing,omitempty" tfsdk:"processing"` } diff --git a/platform_config_gcp.go b/platform_config_gcp.go index 8febcb6..2a65f60 100644 --- a/platform_config_gcp.go +++ b/platform_config_gcp.go @@ -35,8 +35,8 @@ type GcpServiceAccountWorkloadIdentityConfig struct { } type GcpPlatformRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + GcpRole string `json:"gcpRole" tfsdk:"gcp_role"` } type GcpMeteringConfig struct { diff --git a/platform_config_openshift.go b/platform_config_openshift.go index 49587d7..123c6d8 100644 --- a/platform_config_openshift.go +++ b/platform_config_openshift.go @@ -24,6 +24,6 @@ type OpenShiftMeteringConfig struct { } type OpenShiftPlatformRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - OpenshiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + OpenshiftRole string `json:"openshiftRole" tfsdk:"openshift_role"` } diff --git a/platform_properties_aks.go b/platform_properties_aks.go index 4599a5f..04870c3 100644 --- a/platform_properties_aks.go +++ b/platform_properties_aks.go @@ -5,6 +5,6 @@ type AksPlatformProperties struct { } type KubernetesRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - PlatformRoles []string `json:"platformRoles" tfsdk:"platform_roles"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + PlatformRoles []string `json:"platformRoles" tfsdk:"platform_roles"` } diff --git a/platform_properties_aws.go b/platform_properties_aws.go index bbd8480..41a747a 100644 --- a/platform_properties_aws.go +++ b/platform_properties_aws.go @@ -8,7 +8,7 @@ type AwsPlatformProperties struct { } type AwsRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - PlatformRole string `json:"platformRole" tfsdk:"platform_role"` - Policies []string `json:"policies" tfsdk:"policies"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + PlatformRole string `json:"platformRole" tfsdk:"platform_role"` + Policies []string `json:"policies" tfsdk:"policies"` } diff --git a/platform_properties_azure.go b/platform_properties_azure.go index 6979403..2309857 100644 --- a/platform_properties_azure.go +++ b/platform_properties_azure.go @@ -6,7 +6,7 @@ type AzurePlatformProperties struct { } type AzureRoleMappingProperty struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` AzureGroupSuffix string `json:"azureGroupSuffix" tfsdk:"azure_group_suffix"` AzureRoleDefinitions []AzureRoleDefinition `json:"azureRoleDefinitions" tfsdk:"azure_role_definitions"` } diff --git a/platform_properties_azurerg.go b/platform_properties_azurerg.go index dc97e8f..4bc2b70 100644 --- a/platform_properties_azurerg.go +++ b/platform_properties_azurerg.go @@ -7,9 +7,9 @@ type AzureRgPlatformProperties struct { } type AzureRgRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - AzureGroupSuffix string `json:"azureGroupSuffix" tfsdk:"azure_group_suffix"` - AzureRoleDefinitionIds []string `json:"azureRoleDefinitionIds" tfsdk:"azure_role_definition_ids"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + AzureGroupSuffix string `json:"azureGroupSuffix" tfsdk:"azure_group_suffix"` + AzureRoleDefinitionIds []string `json:"azureRoleDefinitionIds" tfsdk:"azure_role_definition_ids"` } type AzureFunction struct { diff --git a/platform_properties_gcp.go b/platform_properties_gcp.go index c8bb02b..f10ae34 100644 --- a/platform_properties_gcp.go +++ b/platform_properties_gcp.go @@ -7,6 +7,6 @@ type GcpPlatformProperties struct { } type GcpRoleMapping struct { - MeshProjectRoleRef MeshProjectRoleRefV2 `json:"projectRoleRef" tfsdk:"project_role_ref"` - PlatformRoles []string `json:"platformRoles" tfsdk:"platform_roles"` + MeshProjectRoleRef NamedRef `json:"projectRoleRef" tfsdk:"project_role_ref"` + PlatformRoles []string `json:"platformRoles" tfsdk:"platform_roles"` } diff --git a/project_binding.go b/project_binding.go index 4178b68..df1529d 100644 --- a/project_binding.go +++ b/project_binding.go @@ -11,17 +11,12 @@ type MeshProjectBindingMetadata struct { Name string `json:"name" tfsdk:"name"` } -// Deprecated: Use MeshProjectRoleRefV2 if possible. The convention is to also provide the `kind`, +// Deprecated: Use NamedRef if possible. The convention is to also provide the `kind`, // so this struct should only be used for meshobjects that violate our API conventions. type MeshProjectRoleRef struct { Name string `json:"name" tfsdk:"name"` } -type MeshProjectRoleRefV2 struct { - Name string `json:"name" tfsdk:"name"` - Kind string `json:"kind" tfsdk:"kind"` -} - type MeshProjectTargetRef struct { Name string `json:"name" tfsdk:"name"` OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` diff --git a/refs.go b/refs.go new file mode 100644 index 0000000..0f68eb2 --- /dev/null +++ b/refs.go @@ -0,0 +1,19 @@ +package client + +// NamedRef is the client-side DTO for a meshObject reference that identifies its +// target by name. It is the counterpart to the meshRefByName schema builder in +// internal/provider (schema_utils.go): every {name, kind} reference block on the +// wire deserializes into this struct. Refs that carry extra fields embed it. +type NamedRef struct { + Name string `json:"name" tfsdk:"name"` + Kind string `json:"kind" tfsdk:"kind"` +} + +// UuidRef is the client-side DTO for a meshObject reference that identifies its +// target by uuid. It is the counterpart to the meshRefByUuid schema builder in +// internal/provider (schema_utils.go): every {uuid, kind} reference block on the +// wire deserializes into this struct. Refs that carry extra fields embed it. +type UuidRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + Kind string `json:"kind" tfsdk:"kind"` +} From 71d0aa0eeb14083088fbe720d710dfc525e0a39c Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Fri, 17 Jul 2026 13:51:25 +0200 Subject: [PATCH 167/200] refactor: model platform aws/gcp role mappings as sets The backend stores AWS (aws_sso, aws_identity_store) and GCP platform role mappings in a map keyed by the referenced meshProjectRole, so their order is neither meaningful nor preserved across a round-trip. Modeling them as ordered ListNestedAttributes therefore produced a permanent no-op plan diff whenever the backend returned them in a different order. Model them as SetNestedAttributes (matching azure/openshift role mappings), retyping the client DTO fields to the generic client/types.Set[T] so the DTO->model converter emits set values, and mark the nested project_role_ref InSet so its identifier stays lenient inside the set. Co-Authored-By: Claude Opus 4.8 (1M context) --- platform_config_aws.go | 22 +++++++++++----------- platform_config_gcp.go | 26 +++++++++++++------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/platform_config_aws.go b/platform_config_aws.go index a45a1d2..55fa205 100644 --- a/platform_config_aws.go +++ b/platform_config_aws.go @@ -48,12 +48,12 @@ type AwsWorkloadIdentityCredential struct { } type AwsSsoConfig struct { - ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` - Arn string `json:"arn" tfsdk:"arn"` - GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - SsoAccessToken types.Secret `json:"ssoAccessToken" tfsdk:"sso_access_token"` - AwsRoleMappings []AwsSsoRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` - SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` + ScimEndpoint string `json:"scimEndpoint" tfsdk:"scim_endpoint"` + Arn string `json:"arn" tfsdk:"arn"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + SsoAccessToken types.Secret `json:"ssoAccessToken" tfsdk:"sso_access_token"` + AwsRoleMappings types.Set[AwsSsoRoleMapping] `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` + SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` } type AwsSsoRoleMapping struct { @@ -68,11 +68,11 @@ type AwsEnrollmentConfiguration struct { } type AwsIdentityStoreConfig struct { - IdentityStoreId string `json:"identityStoreId" tfsdk:"identity_store_id"` - Arn string `json:"arn" tfsdk:"arn"` - GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - AwsRoleMappings []AwsIdentityStoreRoleMapping `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` - SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` + IdentityStoreId string `json:"identityStoreId" tfsdk:"identity_store_id"` + Arn string `json:"arn" tfsdk:"arn"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + AwsRoleMappings types.Set[AwsIdentityStoreRoleMapping] `json:"awsRoleMappings" tfsdk:"aws_role_mappings"` + SignInUrl string `json:"signInUrl" tfsdk:"sign_in_url"` } type AwsIdentityStoreRoleMapping struct { diff --git a/platform_config_gcp.go b/platform_config_gcp.go index 2a65f60..3c27767 100644 --- a/platform_config_gcp.go +++ b/platform_config_gcp.go @@ -8,19 +8,19 @@ type GcpPlatformConfig struct { } type GcpReplicationConfig struct { - ServiceAccount GcpServiceAccountConfig `json:"serviceAccount" tfsdk:"service_account"` - Domain string `json:"domain" tfsdk:"domain"` - CustomerId string `json:"customerId" tfsdk:"customer_id"` - GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` - ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` - ProjectIdPattern string `json:"projectIdPattern" tfsdk:"project_id_pattern"` - BillingAccountId string `json:"billingAccountId" tfsdk:"billing_account_id"` - UserLookupStrategy string `json:"userLookupStrategy" tfsdk:"user_lookup_strategy"` - UsedExternalIdType *string `json:"usedExternalIdType,omitempty" tfsdk:"used_external_id_type"` - GcpRoleMappings []GcpPlatformRoleMapping `json:"gcpRoleMappings" tfsdk:"gcp_role_mappings"` - AllowHierarchicalFolderAssignment bool `json:"allowHierarchicalFolderAssignment" tfsdk:"allow_hierarchical_folder_assignment"` - TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` - SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` + ServiceAccount GcpServiceAccountConfig `json:"serviceAccount" tfsdk:"service_account"` + Domain string `json:"domain" tfsdk:"domain"` + CustomerId string `json:"customerId" tfsdk:"customer_id"` + GroupNamePattern string `json:"groupNamePattern" tfsdk:"group_name_pattern"` + ProjectNamePattern string `json:"projectNamePattern" tfsdk:"project_name_pattern"` + ProjectIdPattern string `json:"projectIdPattern" tfsdk:"project_id_pattern"` + BillingAccountId string `json:"billingAccountId" tfsdk:"billing_account_id"` + UserLookupStrategy string `json:"userLookupStrategy" tfsdk:"user_lookup_strategy"` + UsedExternalIdType *string `json:"usedExternalIdType,omitempty" tfsdk:"used_external_id_type"` + GcpRoleMappings types.Set[GcpPlatformRoleMapping] `json:"gcpRoleMappings" tfsdk:"gcp_role_mappings"` + AllowHierarchicalFolderAssignment bool `json:"allowHierarchicalFolderAssignment" tfsdk:"allow_hierarchical_folder_assignment"` + TenantTags *MeshTenantTags `json:"tenantTags,omitempty" tfsdk:"tenant_tags"` + SkipUserGroupPermissionCleanup bool `json:"skipUserGroupPermissionCleanup" tfsdk:"skip_user_group_permission_cleanup"` } type GcpServiceAccountConfig struct { From 84640316e0360015ce8df294b854cd38738cbecf Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 20 Jul 2026 13:22:02 +0200 Subject: [PATCH 168/200] feat: back meshstack_tenant and meshstack_tenants with the meshTenant v4 API Move the unsuffixed meshstack_tenant resource and meshstack_tenant/meshstack_tenants data sources onto the meshTenant v4 body, referencing platform and landing zone by ref (spec.platform_ref by uuid, spec.landing_zone_ref by name) via the shared meshRef helper, with a computed ref, richer status, and a wait_for_completion toggle. State conversion goes through the generic converter layer (internal/types/generic) reusing the client.MeshTenant DTO directly rather than a hand-rolled model; spec.quotas is a client/types.Set so it renders as the SetNestedAttribute set. Existing v3 state is upgraded in place by an UpgradeState that re-reads the tenant from the v4 API; a moved block from the deprecated meshstack_tenant_v4 is supported (the mover carries the uuid, the post-move refresh re-reads the ref body), so neither path recreates the tenant. Import accepts a tenant UUID or the legacy workspace.project.platform.location composite. The ref-based client.MeshTenant is co-located with the identifier-based client.MeshTenantV4 (that still backs the deprecated meshstack_tenant_v4) in client/tenant_v4.go. --- tenant.go | 78 ------------------------------- tenant_v4.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 78 deletions(-) delete mode 100644 tenant.go diff --git a/tenant.go b/tenant.go deleted file mode 100644 index 3aa4937..0000000 --- a/tenant.go +++ /dev/null @@ -1,78 +0,0 @@ -package client - -import ( - "context" - - "github.com/meshcloud/terraform-provider-meshstack/client/internal" -) - -type MeshTenant struct { - Metadata MeshTenantMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantSpec `json:"spec" tfsdk:"spec"` -} - -type MeshTenantMetadata struct { - OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` - AssignedTags map[string][]string `json:"assignedTags" tfsdk:"assigned_tags"` - DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` -} - -type MeshTenantSpec struct { - LocalId *string `json:"localId" tfsdk:"local_id"` - LandingZoneIdentifier string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` - Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` -} - -type MeshTenantQuota struct { - Key string `json:"key" tfsdk:"key"` - Value int64 `json:"value" tfsdk:"value"` -} - -type MeshTenantCreate struct { - Metadata MeshTenantCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantCreateSpec `json:"spec" tfsdk:"spec"` -} - -type MeshTenantCreateMetadata struct { - OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` -} - -type MeshTenantCreateSpec struct { - LocalId *string `json:"localId" tfsdk:"local_id"` - LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` - Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` -} - -type MeshTenantClient interface { - Read(ctx context.Context, workspace string, project string, platform string) (*MeshTenant, error) - Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) - Delete(ctx context.Context, workspace string, project string, platform string) error -} - -type meshTenantClient struct { - meshObject internal.MeshObjectClient[MeshTenant] -} - -func newTenantClient(ctx context.Context, httpClient internal.HttpClient) MeshTenantClient { - return meshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v3")} -} - -func (c meshTenantClient) tenantId(workspace string, project string, platform string) string { - return workspace + "." + project + "." + platform -} - -func (c meshTenantClient) Read(ctx context.Context, workspace string, project string, platform string) (*MeshTenant, error) { - return c.meshObject.Get(ctx, c.tenantId(workspace, project, platform)) -} - -func (c meshTenantClient) Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) { - return c.meshObject.Post(ctx, tenant) -} - -func (c meshTenantClient) Delete(ctx context.Context, workspace string, project string, platform string) error { - return c.meshObject.Delete(ctx, c.tenantId(workspace, project, platform)) -} diff --git a/tenant_v4.go b/tenant_v4.go index 1968a10..697d43f 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/types" ) type MeshTenantV4 struct { @@ -132,3 +133,132 @@ func (tenant *MeshTenantV4) CreationSuccessful() (done bool, err error) { func (tenant *MeshTenantV4) DeletionSuccessful() (done bool, err error) { return tenant == nil, nil } + +type MeshTenant struct { + Metadata MeshTenantMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantSpec `json:"spec" tfsdk:"spec"` + Status MeshTenantStatus `json:"status" tfsdk:"status"` +} + +type MeshTenantMetadata struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshTenantSpec struct { + PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` + Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` +} + +// MeshTenantStatus has no quotas field; quotas are part of the tenant spec, not its status. +type MeshTenantStatus struct { + TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` + PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` + PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +type MeshTenantQuota struct { + Key string `json:"key" tfsdk:"key"` + Value int64 `json:"value" tfsdk:"value"` +} + +type MeshTenantCreate struct { + Metadata MeshTenantCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshTenantCreateSpec `json:"spec" tfsdk:"spec"` +} + +type MeshTenantCreateMetadata struct { + OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` +} + +type MeshTenantCreateSpec struct { + PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` + LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` +} + +type MeshTenantQuery struct { + Workspace string + Project *string + Platform *string + PlatformType *string + LandingZone *string + PlatformTenant *string +} + +type MeshTenantClient interface { + Read(ctx context.Context, uuid string) (*MeshTenant, error) + ReadFunc(uuid string) func(ctx context.Context) (*MeshTenant, error) + List(ctx context.Context, query *MeshTenantQuery) ([]MeshTenant, error) + Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) + Delete(ctx context.Context, uuid string) error +} + +type meshTenantClient struct { + meshObject internal.MeshObjectClient[MeshTenant] +} + +func newTenantClient(ctx context.Context, httpClient internal.HttpClient) MeshTenantClient { + return meshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v4-preview")} +} + +func (c meshTenantClient) Read(ctx context.Context, uuid string) (*MeshTenant, error) { + return c.ReadFunc(uuid)(ctx) +} + +func (c meshTenantClient) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenant, error) { + return func(ctx context.Context) (*MeshTenant, error) { + return c.meshObject.Get(ctx, uuid) + } +} + +func (c meshTenantClient) Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) { + return c.meshObject.Post(ctx, tenant) +} + +func (c meshTenantClient) List(ctx context.Context, query *MeshTenantQuery) ([]MeshTenant, error) { + options := []internal.RequestOption{ + internal.WithUrlQuery("workspaceIdentifier", query.Workspace), + } + if query.Project != nil { + options = append(options, internal.WithUrlQuery("projectIdentifier", *query.Project)) + } + if query.Platform != nil { + options = append(options, internal.WithUrlQuery("platformIdentifier", *query.Platform)) + } + if query.PlatformType != nil { + options = append(options, internal.WithUrlQuery("platformTypeIdentifier", *query.PlatformType)) + } + if query.LandingZone != nil { + options = append(options, internal.WithUrlQuery("landingZoneIdentifier", *query.LandingZone)) + } + if query.PlatformTenant != nil { + options = append(options, internal.WithUrlQuery("platformTenantId", *query.PlatformTenant)) + } + return c.meshObject.List(ctx, options...) +} + +func (c meshTenantClient) Delete(ctx context.Context, uuid string) error { + return c.meshObject.Delete(ctx, uuid) +} + +func (tenant *MeshTenant) CreationSuccessful() (done bool, err error) { + switch { + case tenant == nil: + err = fmt.Errorf("tenant not found after creation") + case tenant.Spec.PlatformTenantId != nil && *tenant.Spec.PlatformTenantId != "": + // Creation is complete (platformTenantId is set and not empty) + done = true + } + return +} + +func (tenant *MeshTenant) DeletionSuccessful() (done bool, err error) { + return tenant == nil, nil +} From 3cd734605e0034b665f66009dd33c5e06412db6a Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 22 Jul 2026 07:46:17 +0200 Subject: [PATCH 169/200] fix: source meshstack_tenant status from the v4 tenantName field The meshTenant v4 API introduced status.tenantName (superseding tenantIdentifier, which is retained as a deprecated copy while v4 is in preview and dropped at the GA cutover). Point the canonical meshstack_tenant / meshstack_tenants status mapping at tenantName so the provider keeps working once the backend removes tenantIdentifier. The public tenant_identifier attribute is unchanged: the value (the qualified tenant identifier) is identical, so this is behaviour-preserving on the preview API. Co-Authored-By: Claude Opus 4.8 (1M context) --- tenant_v4.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tenant_v4.go b/tenant_v4.go index 697d43f..50fd745 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -155,7 +155,11 @@ type MeshTenantSpec struct { // MeshTenantStatus has no quotas field; quotas are part of the tenant spec, not its status. type MeshTenantStatus struct { - TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` + // Sourced from the backend's status.tenantName (the qualified tenant identifier). The v4 GA + // API renamed tenantIdentifier -> tenantName and drops the deprecated tenantIdentifier; the + // public tenant_identifier attribute keeps its name since the value (qualifiedTenantIdentifier) + // is unchanged. + TenantIdentifier string `json:"tenantName" tfsdk:"tenant_identifier"` PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` Tags map[string][]string `json:"tags" tfsdk:"tags"` From c045ae8cab8ba55dce203655806cf07a133db445 Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Wed, 22 Jul 2026 10:06:09 +0200 Subject: [PATCH 170/200] fix: track only user-declared tags on taggable resources meshStack returns a tag superset on create: an entry for every defined tag property (empty list when unset), plus injected restricted-tag defaults on meshProject / meshLandingZone / meshBuildingBlockDefinition. Writing that superset into the Optional+Computed `tags` attribute broke plan/apply consistency (meshstack_landingzone crashed with "Provider produced inconsistent result after apply") and produced perpetual drift (meshstack_project) for tags the caller may not be permitted to manage. Create/Update now persist the plan's tags; Read reconciles the API response down to the keys already tracked in state (reconcileTrackedTags), so server-injected entries never enter `tags` or surface as drift. Import keeps the full set (there is no prior state to reconcile against). Covers meshstack_landingzone / _project / _workspace / _payment_method / _building_block_definition with real-backend acceptance subtests (restricted-default and superset cases) and a pure reconcileTags unit test. Co-Authored-By: Claude Opus 4.8 (1M context) --- tenant_v4.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tenant_v4.go b/tenant_v4.go index 50fd745..697d43f 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -155,11 +155,7 @@ type MeshTenantSpec struct { // MeshTenantStatus has no quotas field; quotas are part of the tenant spec, not its status. type MeshTenantStatus struct { - // Sourced from the backend's status.tenantName (the qualified tenant identifier). The v4 GA - // API renamed tenantIdentifier -> tenantName and drops the deprecated tenantIdentifier; the - // public tenant_identifier attribute keeps its name since the value (qualifiedTenantIdentifier) - // is unchanged. - TenantIdentifier string `json:"tenantName" tfsdk:"tenant_identifier"` + TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` Tags map[string][]string `json:"tags" tfsdk:"tags"` From 74b5fa933760c7ca2eedad17cf7da4b038c3d0fd Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 21 Jul 2026 20:07:57 +0200 Subject: [PATCH 171/200] feat: send building block definition dependencies as dependencyDefinitionRefs Switch the version-spec dependency wire field from the deprecated dependencyDefinitionUuids (bare UUID array) to dependencyDefinitionRefs ([{uuid, kind}]) so a dependency's kind round-trips. Stays on the current v1-preview media type; the resource schema is unchanged (no config or state migration). The client field is now types.Set[client.UuidRef], so no custom converter is needed. This is the coordinated provider adaptation (per the terraform-provider-compat handshake) for the backend dropping the deprecated dependencyDefinitionUuids: released providers read/write it and would silently stop round-tripping dependencies once the backend removes it, so this must be released and adopted before that removal deploys. Requires meshStack 2026.29.0 (which serves dependencyDefinitionRefs on the preview BBD-version API); MinMeshStackVersion bumped accordingly. Content-hash bumped to v4 (v3 taken by display_order on main); v3 hashes read as incomparable and are gracefully recomputed. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_definition_version.go | 8 ++++---- client.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index 7212dde..732f2c6 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -153,7 +153,6 @@ type MeshBuildingBlockDefinitionVersionMetadata struct { CreatedOn string `json:"createdOn"` } -type BuildingBlockDependencyRef string type MeshBuildingBlockDefinitionVersionSpec struct { BuildingBlockDefinitionRef *UuidRef `json:"buildingBlockDefinitionRef" tfsdk:"-"` OnlyApplyOncePerTenant bool `json:"onlyApplyOncePerTenant" tfsdk:"only_apply_once_per_tenant"` @@ -163,9 +162,10 @@ type MeshBuildingBlockDefinitionVersionSpec struct { VersionNumber *int64 `json:"versionNumber,omitempty" tfsdk:"version_number"` State *MeshBuildingBlockDefinitionVersionState `json:"state,omitempty" tfsdk:"state"` RunnerRef *UuidRef `json:"runnerRef" tfsdk:"runner_ref"` - DependencyDefinitionUUIDs types.Set[BuildingBlockDependencyRef] `json:"dependencyDefinitionUuids,omitempty" tfsdk:"dependency_refs"` - Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` - Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` + // Replaces the deprecated bare-UUID dependencyDefinitionUuids; requires a backend serving it. + DependencyDefinitionRefs types.Set[UuidRef] `json:"dependencyDefinitionRefs,omitempty" tfsdk:"dependency_refs"` + Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` + Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` } type MeshBuildingBlockDefinitionVersionStatus struct { diff --git a/client.go b/client.go index b366b31..93d3387 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.24.0") +var MinMeshStackVersion = version.MustParse("2026.29.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. From f95cf671f471cadd25ee7d8507065ed239c7597b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 22 Jul 2026 13:33:56 +0200 Subject: [PATCH 172/200] refactor: derive List query params from struct json tags in WithUrlQuery WithUrlQuery now takes a single value instead of a (key, value) pair: a filter struct (passed by value) whose json tags name the query params, or a map[string]string / map[string]any taken verbatim. The value is JSON-marshalled and flattened; for a struct, zero-value fields are dropped as an implicit omitempty, so an unset filter needs neither a pointer nor an omitempty tag and a zero-value struct adds no params. Maps keep every entry (e.g. page=0) and round-trip unchanged. Each List client that filters by a query/filter struct now hands that struct to WithUrlQuery by value; the query structs carry json tags naming their params. An error while flattening a query is surfaced on the request instead of building a partial one. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_definition.go | 18 ++++--- building_block_definition_version.go | 8 +++- building_block_v2.go | 58 +++++++--------------- internal/http_client.go | 3 ++ internal/http_client_test.go | 40 ++++++++++++++-- internal/mesh_object_client.go | 2 +- internal/options.go | 50 ++++++++++++++----- platform_type.go | 17 +++---- project.go | 16 ++++--- service_instance.go | 34 ++++--------- tenant_v4.go | 72 +++++++--------------------- 11 files changed, 159 insertions(+), 159 deletions(-) diff --git a/building_block_definition.go b/building_block_definition.go index 46f062a..9ef2e7f 100644 --- a/building_block_definition.go +++ b/building_block_definition.go @@ -76,13 +76,19 @@ func newBuildingBlockDefinitionClient(ctx context.Context, httpClient internal.H } } +type meshBuildingBlockDefinitionListQuery struct { + // IncludeAllPublished is always true here: list definitions published across the platform in + // addition to the workspace's own. (A false bool would be dropped by WithUrlQuery, which is fine — + // this endpoint is only ever called with it set.) + IncludeAllPublished bool `json:"includeAllPublished"` + OwnedByWorkspace *string `json:"ownedByWorkspace"` +} + func (c meshBuildingBlockDefinitionClient) List(ctx context.Context, workspaceIdentifier *string) ([]MeshBuildingBlockDefinition, error) { - var options []internal.RequestOption - options = append(options, internal.WithUrlQuery("includeAllPublished", "true")) - if workspaceIdentifier != nil { - options = append(options, internal.WithUrlQuery("ownedByWorkspace", *workspaceIdentifier)) - } - return c.meshObject.List(ctx, options...) + return c.meshObject.List(ctx, internal.WithUrlQuery(meshBuildingBlockDefinitionListQuery{ + IncludeAllPublished: true, + OwnedByWorkspace: workspaceIdentifier, + })) } func (c meshBuildingBlockDefinitionClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlockDefinition, error) { diff --git a/building_block_definition_version.go b/building_block_definition_version.go index 732f2c6..d63654b 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -198,8 +198,14 @@ func newBuildingBlockDefinitionVersionClient(ctx context.Context, httpClient int } } +type meshBuildingBlockDefinitionVersionListQuery struct { + BuildingBlockDefinitionUuid string `json:"buildingBlockDefinitionUuid"` +} + func (c meshBuildingBlockDefinitionVersionClient) List(ctx context.Context, buildingBlockDefinitionUuid string) ([]MeshBuildingBlockDefinitionVersion, error) { - return c.meshObject.List(ctx, internal.WithUrlQuery("buildingBlockDefinitionUuid", buildingBlockDefinitionUuid)) + return c.meshObject.List(ctx, internal.WithUrlQuery(meshBuildingBlockDefinitionVersionListQuery{ + BuildingBlockDefinitionUuid: buildingBlockDefinitionUuid, + })) } func (c meshBuildingBlockDefinitionVersionClient) Create(ctx context.Context, ownedByWorkspace string, versionSpec MeshBuildingBlockDefinitionVersionSpec) (*MeshBuildingBlockDefinitionVersion, error) { diff --git a/building_block_v2.go b/building_block_v2.go index 0f4c612..1a2bf19 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -140,33 +140,36 @@ type MeshBuildingBlockOutput struct { // MeshBuildingBlockV2ListFilter holds the optional query filters for listing building blocks // via the v2-preview list endpoint. All scalar fields are nil when unset (omitted from the // query). The backend returns only active building blocks; soft-deleted ones are not listed. +// MeshBuildingBlockV2ListFilter holds the optional filters for the V2 building block list endpoint. +// The json tags are the query param names and must match the backend fetchBuildingBlocksV2 +// @RequestParam names exactly; a typo silently disables the filter. type MeshBuildingBlockV2ListFilter struct { - WorkspaceIdentifier *string - ProjectIdentifier *string - PlatformIdentifier *string - Name *string + WorkspaceIdentifier *string `json:"workspaceIdentifier"` + ProjectIdentifier *string `json:"projectIdentifier"` + PlatformIdentifier *string `json:"platformIdentifier"` + Name *string `json:"name"` // DefinitionUuid filters by the owning building block definition's UUID (not a version). - DefinitionUuid *string + DefinitionUuid *string `json:"definitionUuid"` // VersionUuid filters by a specific building block definition version UUID. - VersionUuid *string + VersionUuid *string `json:"versionUuid"` // VersionNumber filters by the literal definition version number. The backend parses it // leniently, so both "v1" and "1" match version 1. - VersionNumber *string - TenantUuid *string + VersionNumber *string `json:"versionNumber"` + TenantUuid *string `json:"tenantUuid"` // TargetKind filters by target ref kind, one of meshTenant or meshWorkspace. - TargetKind *string - Status *string + TargetKind *string `json:"targetRefKind"` + Status *string `json:"status"` // ManagedByWorkspaceIdentifier and ManagedByDefinitionUuid select the platform-operator // (managed) permission scope: building blocks created from definitions owned by the given // workspace / definition. Requires the MANAGED_BUILDINGBLOCK_LIST authority. - ManagedByWorkspaceIdentifier *string - ManagedByDefinitionUuid *string + ManagedByWorkspaceIdentifier *string `json:"managedByWorkspaceIdentifier"` + ManagedByDefinitionUuid *string `json:"managedByDefinitionUuid"` } type MeshBuildingBlockV2Client interface { Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshBuildingBlockV2, error) - List(ctx context.Context, filter *MeshBuildingBlockV2ListFilter) ([]MeshBuildingBlockV2, error) + List(ctx context.Context, filter MeshBuildingBlockV2ListFilter) ([]MeshBuildingBlockV2, error) Create(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) Update(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) Delete(ctx context.Context, uuid string, purge bool) error @@ -191,33 +194,8 @@ func (c meshBuildingBlockV2Client) ReadFunc(uuid string) func(ctx context.Contex } } -func (c meshBuildingBlockV2Client) List(ctx context.Context, filter *MeshBuildingBlockV2ListFilter) ([]MeshBuildingBlockV2, error) { - var options []internal.RequestOption - - // Map each non-nil scalar filter to its query param. Names must match the backend - // fetchBuildingBlocksV2 @RequestParam names exactly; a typo silently disables the filter. - if filter != nil { - for key, value := range map[string]*string{ - "workspaceIdentifier": filter.WorkspaceIdentifier, - "projectIdentifier": filter.ProjectIdentifier, - "platformIdentifier": filter.PlatformIdentifier, - "name": filter.Name, - "definitionUuid": filter.DefinitionUuid, - "versionUuid": filter.VersionUuid, - "versionNumber": filter.VersionNumber, - "tenantUuid": filter.TenantUuid, - "targetRefKind": filter.TargetKind, - "status": filter.Status, - "managedByWorkspaceIdentifier": filter.ManagedByWorkspaceIdentifier, - "managedByDefinitionUuid": filter.ManagedByDefinitionUuid, - } { - if value != nil { - options = append(options, internal.WithUrlQuery(key, *value)) - } - } - } - - return c.meshObject.List(ctx, options...) +func (c meshBuildingBlockV2Client) List(ctx context.Context, filter MeshBuildingBlockV2ListFilter) ([]MeshBuildingBlockV2, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery(filter)) } func (c meshBuildingBlockV2Client) Create(ctx context.Context, bb *MeshBuildingBlockV2) (*MeshBuildingBlockV2, error) { diff --git a/internal/http_client.go b/internal/http_client.go index f4190b9..f7cecd9 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -66,6 +66,9 @@ func (c HttpClient) doRequest(ctx context.Context, method string, url *url.URL, for _, option := range options { option(&opts) } + if opts.optionErr != nil { + return nil, opts.optionErr + } req, err := c.buildRequest(ctx, method, *url, opts) if err != nil { return nil, err diff --git a/internal/http_client_test.go b/internal/http_client_test.go index 36da3a2..baceaec 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -275,7 +275,8 @@ func TestHttpClient(t *testing.T) { } func TestUrlQueryOptions(t *testing.T) { - t.Run("WithUrlQuery sets query parameters", func(t *testing.T) { + queryFrom := func(t *testing.T, query any) url.Values { + t.Helper() var gotQuery url.Values client := newTestClientWithServer(t, func(resp http.ResponseWriter, req *http.Request) { gotQuery = req.URL.Query() @@ -283,12 +284,41 @@ func TestUrlQueryOptions(t *testing.T) { _, _ = resp.Write([]byte(`"ok"`)) }) _, err := DoRequest[string](t.Context(), client, http.MethodGet, client.RootUrl.JoinPath("list"), - WithUrlQuery("definitionUuid", "abc"), - WithUrlQuery("status", "SUCCEEDED"), + WithUrlQuery(query), ) require.NoError(t, err) - assert.Equal(t, "abc", gotQuery.Get("definitionUuid")) - assert.Equal(t, "SUCCEEDED", gotQuery.Get("status")) + return gotQuery + } + + t.Run("a map is sent verbatim", func(t *testing.T) { + got := queryFrom(t, map[string]string{"definitionUuid": "abc", "status": "SUCCEEDED"}) + assert.Equal(t, "abc", got.Get("definitionUuid")) + assert.Equal(t, "SUCCEEDED", got.Get("status")) + }) + + t.Run("map values are kept even when zero", func(t *testing.T) { + got := queryFrom(t, map[string]any{"page": 0}) + assert.Equal(t, "0", got.Get("page")) + }) + + t.Run("struct fields are named by json tag and zero fields are dropped", func(t *testing.T) { + type filter struct { + Identifier *string `json:"identifier"` + Name string `json:"name"` + Restricted *bool `json:"restricted"` + } + got := queryFrom(t, filter{Identifier: new("abc")}) + assert.Equal(t, "abc", got.Get("identifier")) + assert.False(t, got.Has("name"), "zero string field must be dropped") + assert.False(t, got.Has("restricted"), "nil pointer field must be dropped") + }) + + t.Run("a zero-value struct adds no params", func(t *testing.T) { + type filter struct { + Identifier *string `json:"identifier"` + } + got := queryFrom(t, &filter{}) + assert.Empty(t, got) }) } diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index 0fc5b0b..eec05f7 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -146,7 +146,7 @@ func (c MeshObjectClient[M]) List(ctx context.Context, options ...RequestOption) } response, err := DoAuthorizedRequest[paginatedResponse](ctx, c.HttpClient, http.MethodGet, c.ApiUrl, append(options, WithAccept(c.MeshObjectMimeType()), - WithUrlQuery("page", pageNumber), + WithUrlQuery(map[string]any{"page": pageNumber}), )...) if err != nil { return result, fmt.Errorf("error getting page %d: %w", pageNumber, err) diff --git a/internal/options.go b/internal/options.go index 26d41cf..c06e8ba 100644 --- a/internal/options.go +++ b/internal/options.go @@ -1,8 +1,11 @@ package internal import ( + "bytes" + "encoding/json" "fmt" "net/http" + "reflect" ) type ( @@ -14,24 +17,49 @@ type ( extraPathElems []string requestPayload any requestModifiers []requestModifier + // optionErr holds the first error produced while applying options (e.g. an unmarshalable + // query); doRequest surfaces it instead of building a request from partial options. + optionErr error } requestModifier func(req *http.Request) ) -// WithUrlQuery adds a URL query parameter to the request. -// The value is stringified using fmt.Stringer.String() if implemented, otherwise fmt.Sprintf("%v", value). -func WithUrlQuery(key string, value any) RequestOption { +// WithUrlQuery adds URL query parameters from a query value. +// +// The value is JSON-marshalled and decoded into a flat map, so each field becomes a query param +// named by its `json` tag. A struct passed by value is the common case: its zero-value fields are +// dropped (an implicit `omitempty`), so an unset filter needs neither a pointer nor an `omitempty` +// tag and a zero-value struct adds no params at all. A map[string]string / map[string]any is taken +// verbatim — every entry is sent, including deliberate zero values such as page=0. +// +// Values are stringified with fmt.Sprintf("%v", ...); nested objects or arrays are not supported. +func WithUrlQuery(query any) RequestOption { return func(opts *requestOptions) { - var valueStr string - if stringerValue, ok := value.(fmt.Stringer); ok { - valueStr = stringerValue.String() - } else { - valueStr = fmt.Sprintf("%v", value) + data, err := json.Marshal(query) + if err != nil { + opts.optionErr = fmt.Errorf("cannot marshal url query of type %T: %w", query, err) + return } - if opts.urlQueryParams == nil { - opts.urlQueryParams = map[string]string{} + // UseNumber keeps integers (e.g. page) from becoming float64 and gaining a ".0" or exponent. + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var params map[string]any + if err := decoder.Decode(¶ms); err != nil { + opts.optionErr = fmt.Errorf("cannot decode url query of type %T into a flat map: %w", query, err) + return + } + // Drop zero-value fields only for a struct (passed by value, not by pointer); a map is + // passed through as given. + skipZero := reflect.ValueOf(query).Kind() == reflect.Struct + for key, value := range params { + if value == nil || (skipZero && reflect.ValueOf(value).IsZero()) { + continue + } + if opts.urlQueryParams == nil { + opts.urlQueryParams = map[string]string{} + } + opts.urlQueryParams[key] = fmt.Sprintf("%v", value) } - opts.urlQueryParams[key] = valueStr } } diff --git a/platform_type.go b/platform_type.go index a13a77f..270a5c6 100644 --- a/platform_type.go +++ b/platform_type.go @@ -75,13 +75,14 @@ func (c meshPlatformTypeClient) Delete(ctx context.Context, name string) error { return c.meshObject.Delete(ctx, name) } +type meshPlatformTypeListQuery struct { + Category *string `json:"category"` + LifecycleStatus *string `json:"lifecycleStatus"` +} + func (c meshPlatformTypeClient) List(ctx context.Context, category *string, lifecycleStatus *string) ([]MeshPlatformType, error) { - var options []internal.RequestOption - if category != nil { - options = append(options, internal.WithUrlQuery("category", *category)) - } - if lifecycleStatus != nil { - options = append(options, internal.WithUrlQuery("lifecycleStatus", *lifecycleStatus)) - } - return c.meshObject.List(ctx, options...) + return c.meshObject.List(ctx, internal.WithUrlQuery(meshPlatformTypeListQuery{ + Category: category, + LifecycleStatus: lifecycleStatus, + })) } diff --git a/project.go b/project.go index 2d4f60d..1f9078b 100644 --- a/project.go +++ b/project.go @@ -59,14 +59,16 @@ func (c meshProjectClient) Read(ctx context.Context, workspace string, name stri return c.meshObject.Get(ctx, c.projectId(workspace, name)) } +type meshProjectListQuery struct { + WorkspaceIdentifier string `json:"workspaceIdentifier"` + PaymentIdentifier *string `json:"paymentIdentifier"` +} + func (c meshProjectClient) List(ctx context.Context, workspaceIdentifier string, paymentMethodIdentifier *string) ([]MeshProject, error) { - options := []internal.RequestOption{ - internal.WithUrlQuery("workspaceIdentifier", workspaceIdentifier), - } - if paymentMethodIdentifier != nil { - options = append(options, internal.WithUrlQuery("paymentIdentifier", *paymentMethodIdentifier)) - } - return c.meshObject.List(ctx, options...) + return c.meshObject.List(ctx, internal.WithUrlQuery(meshProjectListQuery{ + WorkspaceIdentifier: workspaceIdentifier, + PaymentIdentifier: paymentMethodIdentifier, + })) } func (c meshProjectClient) Create(ctx context.Context, project *MeshProjectCreate) (*MeshProject, error) { diff --git a/service_instance.go b/service_instance.go index 50c7dbb..74918e7 100644 --- a/service_instance.go +++ b/service_instance.go @@ -29,7 +29,7 @@ type MeshServiceInstanceSpec struct { type MeshServiceInstanceClient interface { Read(ctx context.Context, instanceId string) (*MeshServiceInstance, error) - List(ctx context.Context, filter *MeshServiceInstanceFilter) ([]MeshServiceInstance, error) + List(ctx context.Context, filter MeshServiceInstanceFilter) ([]MeshServiceInstance, error) } type meshServiceInstanceClient struct { @@ -37,11 +37,11 @@ type meshServiceInstanceClient struct { } type MeshServiceInstanceFilter struct { - WorkspaceIdentifier *string - ProjectIdentifier *string - MarketplaceIdentifier *string - ServiceIdentifier *string - PlanIdentifier *string + WorkspaceIdentifier *string `json:"workspaceIdentifier"` + ProjectIdentifier *string `json:"projectIdentifier"` + MarketplaceIdentifier *string `json:"marketplaceIdentifier"` + ServiceIdentifier *string `json:"serviceIdentifier"` + PlanIdentifier *string `json:"planIdentifier"` } func newServiceInstanceClient(ctx context.Context, httpClient internal.HttpClient) MeshServiceInstanceClient { @@ -52,24 +52,6 @@ func (c meshServiceInstanceClient) Read(ctx context.Context, instanceId string) return c.meshObject.Get(ctx, instanceId) } -func (c meshServiceInstanceClient) List(ctx context.Context, filter *MeshServiceInstanceFilter) ([]MeshServiceInstance, error) { - var options []internal.RequestOption - if filter != nil { - if filter.WorkspaceIdentifier != nil { - options = append(options, internal.WithUrlQuery("workspaceIdentifier", *filter.WorkspaceIdentifier)) - } - if filter.ProjectIdentifier != nil { - options = append(options, internal.WithUrlQuery("projectIdentifier", *filter.ProjectIdentifier)) - } - if filter.MarketplaceIdentifier != nil { - options = append(options, internal.WithUrlQuery("marketplaceIdentifier", *filter.MarketplaceIdentifier)) - } - if filter.ServiceIdentifier != nil { - options = append(options, internal.WithUrlQuery("serviceIdentifier", *filter.ServiceIdentifier)) - } - if filter.PlanIdentifier != nil { - options = append(options, internal.WithUrlQuery("planIdentifier", *filter.PlanIdentifier)) - } - } - return c.meshObject.List(ctx, options...) +func (c meshServiceInstanceClient) List(ctx context.Context, filter MeshServiceInstanceFilter) ([]MeshServiceInstance, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery(filter)) } diff --git a/tenant_v4.go b/tenant_v4.go index 697d43f..7ebc67a 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -55,18 +55,18 @@ type MeshTenantV4CreateSpec struct { } type MeshTenantV4Query struct { - Workspace string - Project *string - Platform *string - PlatformType *string - LandingZone *string - PlatformTenant *string + Workspace string `json:"workspaceIdentifier"` + Project *string `json:"projectIdentifier"` + Platform *string `json:"platformIdentifier"` + PlatformType *string `json:"platformTypeIdentifier"` + LandingZone *string `json:"landingZoneIdentifier"` + PlatformTenant *string `json:"platformTenantId"` } type MeshTenantV4Client interface { Read(ctx context.Context, uuid string) (*MeshTenantV4, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) - List(ctx context.Context, query *MeshTenantV4Query) ([]MeshTenantV4, error) + List(ctx context.Context, query MeshTenantV4Query) ([]MeshTenantV4, error) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) Delete(ctx context.Context, uuid string) error } @@ -93,26 +93,8 @@ func (c meshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Crea return c.meshObject.Post(ctx, tenant) } -func (c meshTenantV4Client) List(ctx context.Context, query *MeshTenantV4Query) ([]MeshTenantV4, error) { - options := []internal.RequestOption{ - internal.WithUrlQuery("workspaceIdentifier", query.Workspace), - } - if query.Project != nil { - options = append(options, internal.WithUrlQuery("projectIdentifier", *query.Project)) - } - if query.Platform != nil { - options = append(options, internal.WithUrlQuery("platformIdentifier", *query.Platform)) - } - if query.PlatformType != nil { - options = append(options, internal.WithUrlQuery("platformTypeIdentifier", *query.PlatformType)) - } - if query.LandingZone != nil { - options = append(options, internal.WithUrlQuery("landingZoneIdentifier", *query.LandingZone)) - } - if query.PlatformTenant != nil { - options = append(options, internal.WithUrlQuery("platformTenantId", *query.PlatformTenant)) - } - return c.meshObject.List(ctx, options...) +func (c meshTenantV4Client) List(ctx context.Context, query MeshTenantV4Query) ([]MeshTenantV4, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery(query)) } func (c meshTenantV4Client) Delete(ctx context.Context, uuid string) error { @@ -184,18 +166,18 @@ type MeshTenantCreateSpec struct { } type MeshTenantQuery struct { - Workspace string - Project *string - Platform *string - PlatformType *string - LandingZone *string - PlatformTenant *string + Workspace string `json:"workspaceIdentifier"` + Project *string `json:"projectIdentifier"` + Platform *string `json:"platformIdentifier"` + PlatformType *string `json:"platformTypeIdentifier"` + LandingZone *string `json:"landingZoneIdentifier"` + PlatformTenant *string `json:"platformTenantId"` } type MeshTenantClient interface { Read(ctx context.Context, uuid string) (*MeshTenant, error) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenant, error) - List(ctx context.Context, query *MeshTenantQuery) ([]MeshTenant, error) + List(ctx context.Context, query MeshTenantQuery) ([]MeshTenant, error) Create(ctx context.Context, tenant *MeshTenantCreate) (*MeshTenant, error) Delete(ctx context.Context, uuid string) error } @@ -222,26 +204,8 @@ func (c meshTenantClient) Create(ctx context.Context, tenant *MeshTenantCreate) return c.meshObject.Post(ctx, tenant) } -func (c meshTenantClient) List(ctx context.Context, query *MeshTenantQuery) ([]MeshTenant, error) { - options := []internal.RequestOption{ - internal.WithUrlQuery("workspaceIdentifier", query.Workspace), - } - if query.Project != nil { - options = append(options, internal.WithUrlQuery("projectIdentifier", *query.Project)) - } - if query.Platform != nil { - options = append(options, internal.WithUrlQuery("platformIdentifier", *query.Platform)) - } - if query.PlatformType != nil { - options = append(options, internal.WithUrlQuery("platformTypeIdentifier", *query.PlatformType)) - } - if query.LandingZone != nil { - options = append(options, internal.WithUrlQuery("landingZoneIdentifier", *query.LandingZone)) - } - if query.PlatformTenant != nil { - options = append(options, internal.WithUrlQuery("platformTenantId", *query.PlatformTenant)) - } - return c.meshObject.List(ctx, options...) +func (c meshTenantClient) List(ctx context.Context, query MeshTenantQuery) ([]MeshTenant, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery(query)) } func (c meshTenantClient) Delete(ctx context.Context, uuid string) error { From 66fe8157dbd09b0555044ee8331b16c93632f483 Mon Sep 17 00:00:00 2001 From: Vadim Zaslavsky Date: Tue, 21 Jul 2026 12:50:59 +0200 Subject: [PATCH 173/200] fix: make declared outputs work on manual building blocks Previously, declaring `version_spec.outputs` on a manual building block failed at apply with "Provider produced inconsistent result after apply": the backend always returns one output per input, while config held only the outputs the user declared, so backend-derived elements "appeared" and the backend-assigned display_order / content_hash disagreed with the plan on create, release, and re-draft. Declared manual outputs are now a sparse override: declare only the outputs you customize (keyed by the matching input); the rest are derived. Config and state hold just your overrides, and the backend's full one-per-input response is pruned to that tracked subset on read-back (keep an output iff its assignment_type != NONE or its display_name differs from the input's). An empty `outputs = {}`, like omitting it, means "no overrides". - type is always backend-derived: rejected in config, derived and sent by the provider (the backend 400s on an empty or mismatching output type). - display_name/display_order are Optional+Computed and held from state via UseStateForUnknown, so a no-op plan stays fully known and the content hash is stable. - The full one-per-input set is sent on every apply, so dropping an override resets it (the backend preserves overrides for keys absent from the request). - ValidateConfig rejects an output with no matching input, a declared type, or a no-op override; non-manual outputs still require type and display_name. - content_hash bumped to v5 (hashing the tracked subset); an older hash is recomputed rather than flagged changed, so upgrading does not rerun already-released blocks. Fixes #131, #176, #240. BD-2594 Co-Authored-By: Claude Opus 4.8 (1M context) --- types/enum/enum.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/types/enum/enum.go b/types/enum/enum.go index a8279e6..0f07fef 100644 --- a/types/enum/enum.go +++ b/types/enum/enum.go @@ -2,7 +2,6 @@ package enum import ( "fmt" - "slices" "strings" ) @@ -29,14 +28,6 @@ func (e Enum[T]) Strings() []string { return e.to(Entry[T].String) } -// Except returns the enum minus the given entries, preserving order. Deriving a subset this way keeps a -// single source of truth: adding an entry to the base enum flows into the subset automatically. -func (e Enum[T]) Except(excluded ...Entry[T]) Enum[T] { - return slices.DeleteFunc(slices.Clone(e), func(ee Entry[T]) bool { - return slices.Contains(excluded, ee) - }) -} - func (e Enum[T]) Markdown() string { return strings.Join(e.to(Entry[T].Markdown), ", ") } From 958d44853217b9c35012032f9e517bb36ffcf469 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 23 Jul 2026 08:07:25 +0200 Subject: [PATCH 174/200] feat!: rename meshstack_tenant status.tenant_identifier to tenant_name Track the GA meshTenant v4 API, which dropped status.tenantIdentifier in favour of status.tenantName. The value is unchanged (the fully-qualified ...); only the attribute/field name changes on meshstack_tenant and the meshstack_tenants data source. Co-Authored-By: Claude Opus 4.8 (1M context) --- tenant_v4.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tenant_v4.go b/tenant_v4.go index 7ebc67a..c5ed378 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -137,7 +137,7 @@ type MeshTenantSpec struct { // MeshTenantStatus has no quotas field; quotas are part of the tenant spec, not its status. type MeshTenantStatus struct { - TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` + TenantName string `json:"tenantName" tfsdk:"tenant_name"` PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` Tags map[string][]string `json:"tags" tfsdk:"tags"` From dc0621d308401d1bab1b12731b8b746539a14c5c Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 23 Jul 2026 16:52:53 +0200 Subject: [PATCH 175/200] chore: apply go1.26 go fix idioms Run `go fix ./...` and take its default fixes: - any: interface{} -> any in a test helper signature - rangeint: for i := 0; i < len(x); i++ -> for i := range x - omitzero: drop dead `,omitempty` on struct-typed TF fields (types.SecretOrAny, types.List). encoding/json never omits struct types, so the tag was a no-op; Variant already marshals the zero value to null. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- building_block_definition_version.go | 4 ++-- version/version_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/building_block_definition_version.go b/building_block_definition_version.go index d63654b..2f84dc9 100644 --- a/building_block_definition_version.go +++ b/building_block_definition_version.go @@ -94,8 +94,8 @@ type MeshBuildingBlockDefinitionInput struct { // Otherwise, the [types.Variant] is of [types.Any] (case [types.Variant.Y]). // As this is a fallback detection when JSON (un)marshaling, // types.Any must go second as [types.Variant] intentionally prefers X over Y. - Argument types.SecretOrAny `json:"argument,omitempty" tfsdk:"argument"` - DefaultValue types.SecretOrAny `json:"defaultValue,omitempty" tfsdk:"default_value"` + Argument types.SecretOrAny `json:"argument" tfsdk:"argument"` + DefaultValue types.SecretOrAny `json:"defaultValue" tfsdk:"default_value"` UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` SelectableValues types.Set[string] `json:"selectableValues,omitempty" tfsdk:"selectable_values"` Description *string `json:"description,omitempty" tfsdk:"description"` diff --git a/version/version_test.go b/version/version_test.go index 9e06840..aa7911b 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -10,7 +10,7 @@ import ( func TestParse(t *testing.T) { assertErrorContainsAllOf := func(contains ...string) assert.ErrorAssertionFunc { - return func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return func(t assert.TestingT, err error, msgAndArgs ...any) bool { assert.NotEmpty(t, contains) allOk := true for _, contain := range contains { From 2999fae2998ec448833327b56da059950db4aaed Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 21 Jul 2026 01:28:11 +0200 Subject: [PATCH 176/200] feat: add List wrappers to platform and landing zone clients Add thin List methods over MeshObjectClient.List (with WithUrlQuery filters, mirroring platform_type/tenant_v4) to MeshPlatformClient and MeshLandingZoneClient, plus matching in-memory List implementations on the mock clients that apply only plain attribute filtering. The mocks deliberately do not simulate marketplace visibility or permissions, so cross-workspace entitlement remains acceptance-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- landingzone.go | 15 +++++++++++++++ platform.go | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/landingzone.go b/landingzone.go index 42474aa..259be05 100644 --- a/landingzone.go +++ b/landingzone.go @@ -58,8 +58,19 @@ type MeshLandingZoneCreate struct { Spec MeshLandingZoneSpec `json:"spec" tfsdk:"spec"` } +// MeshLandingZoneListQuery holds the optional filters for the V1 landing zone list endpoint. The +// json tags name the query params; unset (nil/zero) fields are dropped by WithUrlQuery. +type MeshLandingZoneListQuery struct { + PlatformUuid *string `json:"platformUuid"` + Identifier *string `json:"identifier"` + DisplayName *string `json:"displayName"` + Restricted *bool `json:"restricted"` + OwnedByWorkspace *string `json:"ownedByWorkspace"` +} + type MeshLandingZoneClient interface { Read(ctx context.Context, name string) (*MeshLandingZone, error) + List(ctx context.Context, query MeshLandingZoneListQuery) ([]MeshLandingZone, error) Create(ctx context.Context, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) Update(ctx context.Context, name string, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) Delete(ctx context.Context, name string) error @@ -77,6 +88,10 @@ func (c meshLandingZoneClient) Read(ctx context.Context, name string) (*MeshLand return c.meshObject.Get(ctx, name) } +func (c meshLandingZoneClient) List(ctx context.Context, query MeshLandingZoneListQuery) ([]MeshLandingZone, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery(query)) +} + func (c meshLandingZoneClient) Create(ctx context.Context, landingZone *MeshLandingZoneCreate) (*MeshLandingZone, error) { return c.meshObject.Post(ctx, landingZone) } diff --git a/platform.go b/platform.go index d49d4d3..33fa150 100644 --- a/platform.go +++ b/platform.go @@ -75,8 +75,24 @@ type TagMapper struct { ValuePattern string `json:"valuePattern" tfsdk:"value_pattern"` } +// MeshPlatformListQuery holds the optional filters for the V2 platform list endpoint. The json tags +// name the query params; unset (nil/zero) fields are dropped by WithUrlQuery. +type MeshPlatformListQuery struct { + OwnedByWorkspace *string `json:"ownedByWorkspace"` + Identifier *string `json:"identifier"` + LocationIdentifier *string `json:"locationIdentifier"` + DisplayName *string `json:"displayName"` + Restriction *string `json:"restriction"` + PublicationState *string `json:"publicationState"` + ContributingWorkspace *string `json:"contributingWorkspace"` + // PlatformTypeIdentifier filters by the platform type's identifier (matched backend-side); the type + // is not carried in the response, and spec.config is redacted for marketplace consumers anyway. + PlatformTypeIdentifier *string `json:"platformTypeIdentifier"` +} + type MeshPlatformClient interface { Read(ctx context.Context, uuid string) (*MeshPlatform, error) + List(ctx context.Context, query MeshPlatformListQuery) ([]MeshPlatform, error) Create(ctx context.Context, platform MeshPlatform) (*MeshPlatform, error) Update(ctx context.Context, uuid string, platform MeshPlatform) (*MeshPlatform, error) Delete(ctx context.Context, uuid string) error @@ -94,6 +110,10 @@ func (c meshPlatformClient) Read(ctx context.Context, uuid string) (*MeshPlatfor return c.meshObject.Get(ctx, uuid) } +func (c meshPlatformClient) List(ctx context.Context, query MeshPlatformListQuery) ([]MeshPlatform, error) { + return c.meshObject.List(ctx, internal.WithUrlQuery(query)) +} + func (c meshPlatformClient) Create(ctx context.Context, platform MeshPlatform) (*MeshPlatform, error) { return c.meshObject.Post(ctx, platform) } From ee819a2cb5a31cd962bbd228c80a3fc6d362442d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 21 Jul 2026 01:28:24 +0200 Subject: [PATCH 177/200] feat: add meshstack_platforms and meshstack_landingzones data sources New plural (list) data sources so a platform or landing zone can be resolved by name, identifier, type or publication state in HCL instead of hardcoding a UUID. Each element reuses the singular data source's element schema (extracted into shared schema helpers) and its per-element platformModelFromDto / landingZoneModelFrom mapping, including the computed ref that drops straight into meshstack_tenant's platform_ref / landing_zone_ref. meshstack_platforms exposes the marketplace/discovery filters (publication_state, restriction, platform_type as a PlatformCategory, owned_by_workspace, ...) and, on a backend that supports it, also returns platforms published to the caller's workspace; meshstack_landingzones exposes platform_uuid to list a chosen platform's landing zones. spec.config is Computed and may be omitted for a platform the caller only consumes cross-workspace; the singular and plural platform data source descriptions document this. Co-Authored-By: Claude Opus 4.8 (1M context) --- platform.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/platform.go b/platform.go index 33fa150..9c0bbad 100644 --- a/platform.go +++ b/platform.go @@ -19,17 +19,18 @@ type MeshPlatformMetadata struct { } type MeshPlatformSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - Endpoint string `json:"endpoint" tfsdk:"endpoint"` - SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` - DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` - AccessInformation *string `json:"accessInformation,omitempty" tfsdk:"access_information"` - LocationRef NamedRef `json:"locationRef" tfsdk:"location_ref"` - ContributingWorkspaces types.Set[string] `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` - Availability PlatformAvailability `json:"availability" tfsdk:"availability"` - Config PlatformConfig `json:"config" tfsdk:"config"` - QuotaDefinitions types.Set[QuotaDefinition] `json:"quotaDefinitions" tfsdk:"quota_definitions"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + Endpoint string `json:"endpoint" tfsdk:"endpoint"` + SupportUrl *string `json:"supportUrl,omitempty" tfsdk:"support_url"` + DocumentationUrl *string `json:"documentationUrl,omitempty" tfsdk:"documentation_url"` + AccessInformation *string `json:"accessInformation,omitempty" tfsdk:"access_information"` + LocationRef NamedRef `json:"locationRef" tfsdk:"location_ref"` + ContributingWorkspaces types.Set[string] `json:"contributingWorkspaces" tfsdk:"contributing_workspaces"` + Availability PlatformAvailability `json:"availability" tfsdk:"availability"` + // Config is nullable in responses: redacted (omitted) for marketplace-consumer callers. Required on write. + Config *PlatformConfig `json:"config,omitempty" tfsdk:"config"` + QuotaDefinitions types.Set[QuotaDefinition] `json:"quotaDefinitions" tfsdk:"quota_definitions"` } type QuotaDefinition struct { From 6a1043114a8e1d35bbc9925942c130cbbb3f66d8 Mon Sep 17 00:00:00 2001 From: Vadim Zaslavsky Date: Thu, 23 Jul 2026 16:47:04 +0200 Subject: [PATCH 178/200] feat: expiry date on workspace bindings Workspace group and user bindings now accept the expiry date. --- client.go | 2 +- workspace_binding.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client.go b/client.go index 93d3387..e266727 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.29.0") +var MinMeshStackVersion = version.MustParse("2026.30.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. diff --git a/workspace_binding.go b/workspace_binding.go index fc6a253..30d744e 100644 --- a/workspace_binding.go +++ b/workspace_binding.go @@ -1,10 +1,11 @@ package client type MeshWorkspaceBinding struct { - Metadata MeshWorkspaceBindingMetadata `json:"metadata" tfsdk:"metadata"` - RoleRef MeshWorkspaceRoleRef `json:"roleRef" tfsdk:"role_ref"` - TargetRef MeshWorkspaceTargetRef `json:"targetRef" tfsdk:"target_ref"` - Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` + Metadata MeshWorkspaceBindingMetadata `json:"metadata" tfsdk:"metadata"` + RoleRef MeshWorkspaceRoleRef `json:"roleRef" tfsdk:"role_ref"` + TargetRef MeshWorkspaceTargetRef `json:"targetRef" tfsdk:"target_ref"` + Subject MeshWorkspaceSubject `json:"subject" tfsdk:"subject"` + ExpiryDate *string `json:"expiryDate,omitempty" tfsdk:"expiry_date"` } type MeshWorkspaceBindingMetadata struct { From a6d35b7dbbcd0cff46aefaea5efd8d79ab211ca7 Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Wed, 22 Jul 2026 17:36:55 +0200 Subject: [PATCH 179/200] feat: read back effective tenant quotas from meshTenant v4 status.quotas The meshObject meshTenant v4-preview API now returns the tenant's applied quotas in status.quotas and enforces spec.quotas against each quota's [minValue, maxValue] bounds and auto-approval threshold on create (HTTP 400 on rejection). Consume that contract: - Expose a computed status.quotas set on the meshstack_tenant resource and the meshstack_tenant / meshstack_tenants data sources, sourced from the API instead of being fabricated from spec.quotas. - Switch the deprecated meshstack_tenant_v4 resource/data source to read the real status.quotas as well (schema-compatible; status is computed-only). - spec.quotas stays create-only: changing it on an existing tenant is still rejected at plan time, and the backend's descriptive 400 for out-of-range/above-threshold values bubbles up via the existing create error path. - Bump MinMeshStackVersion to 2026.30.0 (the release carrying the BD-2607 backend). The change is additive, so no terraform-provider-compat registry entry is required. Co-Authored-By: Claude Opus 4.8 (1M context) --- tenant_v4.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tenant_v4.go b/tenant_v4.go index c5ed378..c2259b5 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -35,6 +35,9 @@ type MeshTenantV4Status struct { PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceIdentifier *string `json:"platformWorkspaceIdentifier" tfsdk:"platform_workspace_identifier"` Tags map[string][]string `json:"tags" tfsdk:"tags"` + // Quotas are the effective quotas meshStack applied to the tenant, distinct from the create-only + // spec.quotas which carries only the requested values. + Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } type MeshTenantV4Create struct { @@ -135,12 +138,15 @@ type MeshTenantSpec struct { Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` } -// MeshTenantStatus has no quotas field; quotas are part of the tenant spec, not its status. type MeshTenantStatus struct { TenantName string `json:"tenantName" tfsdk:"tenant_name"` PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` Tags map[string][]string `json:"tags" tfsdk:"tags"` + // Quotas are the effective quotas meshStack applied to the tenant. spec.quotas carries only the + // values requested at create (create-only); the effective quotas here can differ once landing-zone + // defaults are merged in or an operator adjusts them, so drift is tracked against these. + Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` } type MeshTenantQuota struct { From 6ae439db9f609b4919020967464b0d0f23e4e310 Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Thu, 23 Jul 2026 13:00:15 +0200 Subject: [PATCH 180/200] feat: model tenant quotas as maps (requested_quotas / applied_quotas) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the meshTenant v4 API quota rework: - spec: add `requested_quotas` (map(number)); keep `quotas` (list) as a deprecated attribute for backward compatibility - status: `applied_quotas` (map(number)) read back from the API, replacing the unreleased `status.quotas` set — on meshstack_tenant, its data sources, and the deprecated meshstack_tenant_v4 - update client structs, mocks, tests; regenerate docs; changelog Co-Authored-By: Claude Opus 4.8 (1M context) --- tenant_v4.go | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/tenant_v4.go b/tenant_v4.go index c2259b5..eac3f89 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -35,9 +35,9 @@ type MeshTenantV4Status struct { PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceIdentifier *string `json:"platformWorkspaceIdentifier" tfsdk:"platform_workspace_identifier"` Tags map[string][]string `json:"tags" tfsdk:"tags"` - // Quotas are the effective quotas meshStack applied to the tenant, distinct from the create-only - // spec.quotas which carries only the requested values. - Quotas []MeshTenantQuota `json:"quotas" tfsdk:"quotas"` + // AppliedQuotas are the effective quotas meshStack applied to the tenant as a key->value map, + // distinct from the create-only spec.quotas which carries only the requested values. + AppliedQuotas map[string]int64 `json:"appliedQuotas" tfsdk:"applied_quotas"` } type MeshTenantV4Create struct { @@ -132,10 +132,14 @@ type MeshTenantMetadata struct { } type MeshTenantSpec struct { - PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` - PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` - LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` - Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` + PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` + // RequestedQuotas is the preferred key->value form for requesting quotas at creation, e.g. + // {"limits.cpu": 4}. + RequestedQuotas map[string]int64 `json:"requestedQuotas" tfsdk:"requested_quotas"` + // Deprecated: superseded by RequestedQuotas; retained so existing configurations keep working. + Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` } type MeshTenantStatus struct { @@ -143,10 +147,11 @@ type MeshTenantStatus struct { PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` Tags map[string][]string `json:"tags" tfsdk:"tags"` - // Quotas are the effective quotas meshStack applied to the tenant. spec.quotas carries only the - // values requested at create (create-only); the effective quotas here can differ once landing-zone - // defaults are merged in or an operator adjusts them, so drift is tracked against these. - Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` + // AppliedQuotas are the effective quotas meshStack applied to the tenant as a key->value map. + // spec.requested_quotas carries only the values requested at create (create-only); the effective + // quotas here can differ once landing-zone defaults are merged in or an operator adjusts them, so + // drift is tracked against these. + AppliedQuotas map[string]int64 `json:"appliedQuotas" tfsdk:"applied_quotas"` } type MeshTenantQuota struct { @@ -165,10 +170,13 @@ type MeshTenantCreateMetadata struct { } type MeshTenantCreateSpec struct { - PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` - LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` - PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` - Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` + PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` + LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + // RequestedQuotas is the preferred key->value form; Quotas is the deprecated list form. Only one + // should be set — the backend rejects a create that carries both with conflicting values. + RequestedQuotas map[string]int64 `json:"requestedQuotas,omitempty" tfsdk:"requested_quotas"` + Quotas types.Set[MeshTenantQuota] `json:"quotas,omitempty" tfsdk:"quotas"` } type MeshTenantQuery struct { From acd3c49ee9b1d9969d3ce146cdfc515fe68e7ae4 Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Fri, 24 Jul 2026 14:57:26 +0200 Subject: [PATCH 181/200] feat: model tenant quotas as structured value objects and warn on unrealized quotas Address review feedback on the v4 meshTenant quota representation, matching the companion meshfed-release change (BD-2607, branch feature/BD-2607-meshobject-api-ignores-quotas): - Wrap requested_quotas/applied_quotas map values in RequestQuotaValue / AppliedQuotaValue objects ({value}) across meshstack_tenant, its data sources, and the deprecated meshstack_tenant_v4. The map values now match the API and can gain per-quota fields later without a breaking change; the user-facing attributes become maps of objects (e.g. { "limits.cpu" = { value = 4 } }). - Warn (not error) when the requested quotas were not applied verbatim, hinting that landing-zone defaults or a pending platform-operator approval may be the reason. Comparison logic is a pure helper with unit tests. - spec.requested_quotas is a create-time input the API no longer returns on read; the data sources now document it as typically null. Update mocks, tests, changelog, and regenerate docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- tenant_v4.go | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/tenant_v4.go b/tenant_v4.go index eac3f89..fe347b9 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -36,8 +36,10 @@ type MeshTenantV4Status struct { PlatformWorkspaceIdentifier *string `json:"platformWorkspaceIdentifier" tfsdk:"platform_workspace_identifier"` Tags map[string][]string `json:"tags" tfsdk:"tags"` // AppliedQuotas are the effective quotas meshStack applied to the tenant as a key->value map, - // distinct from the create-only spec.quotas which carries only the requested values. - AppliedQuotas map[string]int64 `json:"appliedQuotas" tfsdk:"applied_quotas"` + // distinct from the create-only spec.quotas which carries only the requested values. Each value is a + // structured object (e.g. `{"limits.cpu": {"value": 4}}`) so the preview API can grow per-quota + // fields without a breaking change to the map shape. + AppliedQuotas map[string]AppliedQuotaValue `json:"appliedQuotas" tfsdk:"applied_quotas"` } type MeshTenantV4Create struct { @@ -136,8 +138,9 @@ type MeshTenantSpec struct { PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` // RequestedQuotas is the preferred key->value form for requesting quotas at creation, e.g. - // {"limits.cpu": 4}. - RequestedQuotas map[string]int64 `json:"requestedQuotas" tfsdk:"requested_quotas"` + // {"limits.cpu": {"value": 4}}. The backend does not return it on read (it is a create-time input), + // so the resource echoes the configured value from state. + RequestedQuotas map[string]RequestQuotaValue `json:"requestedQuotas" tfsdk:"requested_quotas"` // Deprecated: superseded by RequestedQuotas; retained so existing configurations keep working. Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` } @@ -147,11 +150,11 @@ type MeshTenantStatus struct { PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` PlatformWorkspaceId *string `json:"platformWorkspaceId" tfsdk:"platform_workspace_id"` Tags map[string][]string `json:"tags" tfsdk:"tags"` - // AppliedQuotas are the effective quotas meshStack applied to the tenant as a key->value map. - // spec.requested_quotas carries only the values requested at create (create-only); the effective - // quotas here can differ once landing-zone defaults are merged in or an operator adjusts them, so - // drift is tracked against these. - AppliedQuotas map[string]int64 `json:"appliedQuotas" tfsdk:"applied_quotas"` + // AppliedQuotas are the effective quotas meshStack applied to the tenant as a key->value map, each + // value a structured object (e.g. `{"limits.cpu": {"value": 4}}`). spec.requested_quotas carries + // only the values requested at create (create-only); the effective quotas here can differ once + // landing-zone defaults are merged in or an operator adjusts them, so drift is tracked against these. + AppliedQuotas map[string]AppliedQuotaValue `json:"appliedQuotas" tfsdk:"applied_quotas"` } type MeshTenantQuota struct { @@ -159,6 +162,20 @@ type MeshTenantQuota struct { Value int64 `json:"value" tfsdk:"value"` } +// RequestQuotaValue is a requested tenant quota value. The scalar is wrapped in an object (rather than +// a bare number) so the v4 preview API can grow per-quota fields — e.g. a unit — without a breaking +// change to the requested_quotas map shape. +type RequestQuotaValue struct { + Value int64 `json:"value" tfsdk:"value"` +} + +// AppliedQuotaValue is a tenant quota value as actually applied by the backend. Kept distinct from +// RequestQuotaValue so it can later carry applied-only context (e.g. why the applied value differs +// from what was requested). +type AppliedQuotaValue struct { + Value int64 `json:"value" tfsdk:"value"` +} + type MeshTenantCreate struct { Metadata MeshTenantCreateMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshTenantCreateSpec `json:"spec" tfsdk:"spec"` @@ -175,8 +192,8 @@ type MeshTenantCreateSpec struct { PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` // RequestedQuotas is the preferred key->value form; Quotas is the deprecated list form. Only one // should be set — the backend rejects a create that carries both with conflicting values. - RequestedQuotas map[string]int64 `json:"requestedQuotas,omitempty" tfsdk:"requested_quotas"` - Quotas types.Set[MeshTenantQuota] `json:"quotas,omitempty" tfsdk:"quotas"` + RequestedQuotas map[string]RequestQuotaValue `json:"requestedQuotas,omitempty" tfsdk:"requested_quotas"` + Quotas types.Set[MeshTenantQuota] `json:"quotas,omitempty" tfsdk:"quotas"` } type MeshTenantQuery struct { From 88079633b4740059a2b0b16fd7c2af066ee4ac7e Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Wed, 29 Jul 2026 15:56:47 +0200 Subject: [PATCH 182/200] fix: correct tenant quota approval semantics, cover landing-zone defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the PR review on the tenant quota map work. The provider claimed a requested quota beyond the platform's auto-approval threshold waits for operator approval. The meshObject API does the opposite: it refuses such a create outright, deliberately, because it has no quota-request representation and a pending request would let an apply report success on quotas that are not in effect. Rewrite the warning and the schema descriptions to document what actually happens — applied quotas are the landing zone's defaults overlaid with the request, and a requested key that differs was changed after creation. Cover the two cases a real backend can produce: a landing-zone default the tenant never requests (applied is a strict superset of requested, asserted to not drift on re-plan) and an above-threshold request being rejected. The tenant mock now overlays landing-zone default quotas as the backend does, so the former runs in both unit and acceptance mode. Also mark the deprecated create-spec quotas field with a godoc marker, document MeshTenantQuota as the deprecated list-form element, state the real reason RequestQuotaValue and AppliedQuotaValue stay distinct types, and shorten the changelog entry. Co-Authored-By: Claude Opus 5 (1M context) --- tenant_v4.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tenant_v4.go b/tenant_v4.go index fe347b9..f8bdacf 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -157,21 +157,27 @@ type MeshTenantStatus struct { AppliedQuotas map[string]AppliedQuotaValue `json:"appliedQuotas" tfsdk:"applied_quotas"` } +// MeshTenantQuota is the {key, value} element of the deprecated list-form spec.quotas, superseded by +// the requested_quotas / applied_quotas maps. It is still the quota shape of the deprecated +// meshstack_tenant_v4 resource, so it carries no godoc deprecation marker. type MeshTenantQuota struct { Key string `json:"key" tfsdk:"key"` Value int64 `json:"value" tfsdk:"value"` } -// RequestQuotaValue is a requested tenant quota value. The scalar is wrapped in an object (rather than -// a bare number) so the v4 preview API can grow per-quota fields — e.g. a unit — without a breaking -// change to the requested_quotas map shape. +// RequestQuotaValue is a tenant quota value as requested at create time. The scalar is wrapped in an +// object (rather than a bare number) so the v4 preview API can grow per-quota fields — e.g. a unit — +// without a breaking change to the requested_quotas map shape. +// +// Its shape is identical to AppliedQuotaValue, deliberately so: the resource must echo the configured +// request in spec while reading effective values from status, and separate types turn mixing the two +// into a compile error rather than the requested-vs-applied conflation this map form fixes. type RequestQuotaValue struct { Value int64 `json:"value" tfsdk:"value"` } -// AppliedQuotaValue is a tenant quota value as actually applied by the backend. Kept distinct from -// RequestQuotaValue so it can later carry applied-only context (e.g. why the applied value differs -// from what was requested). +// AppliedQuotaValue is a tenant quota value as actually applied by the backend. See RequestQuotaValue +// for why the two are not a single type. type AppliedQuotaValue struct { Value int64 `json:"value" tfsdk:"value"` } @@ -190,10 +196,11 @@ type MeshTenantCreateSpec struct { PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` - // RequestedQuotas is the preferred key->value form; Quotas is the deprecated list form. Only one - // should be set — the backend rejects a create that carries both with conflicting values. + // RequestedQuotas is the preferred key->value form. Only one of the two quota fields should be + // set — the backend rejects a create that carries both with conflicting values. RequestedQuotas map[string]RequestQuotaValue `json:"requestedQuotas,omitempty" tfsdk:"requested_quotas"` - Quotas types.Set[MeshTenantQuota] `json:"quotas,omitempty" tfsdk:"quotas"` + // Deprecated: superseded by RequestedQuotas; retained so existing configurations keep working. + Quotas types.Set[MeshTenantQuota] `json:"quotas,omitempty" tfsdk:"quotas"` } type MeshTenantQuery struct { From 82ae669138255e7360d368134177714a45ef67b3 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 30 Jul 2026 18:25:50 +0200 Subject: [PATCH 183/200] chore: fix acceptance tests timeouts at tenant deletion --- tenant_v4.go | 57 +++++++++++++++++++- tenant_v4_test.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 tenant_v4_test.go diff --git a/tenant_v4.go b/tenant_v4.go index f8bdacf..cfbb65a 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -6,6 +6,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/internal" "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) type MeshTenantV4 struct { @@ -40,6 +41,25 @@ type MeshTenantV4Status struct { // structured object (e.g. `{"limits.cpu": {"value": 4}}`) so the preview API can grow per-quota // fields without a breaking change to the map shape. AppliedQuotas map[string]AppliedQuotaValue `json:"appliedQuotas" tfsdk:"applied_quotas"` + Lifecycle MeshTenantLifecycle `json:"lifecycle" tfsdk:"-"` +} + +type TenantLifecycleState string + +var ( + TenantLifecycleStates = enum.Enum[TenantLifecycleState]{} + TenantLifecycleStateActive = TenantLifecycleStates.Entry("ACTIVE") + TenantLifecycleStateMarkedForDeletion = TenantLifecycleStates.Entry("MARKED_FOR_DELETION") + TenantLifecycleStateDeleted = TenantLifecycleStates.Entry("DELETED") +) + +type MeshTenantLifecycle struct { + State enum.Entry[TenantLifecycleState] `json:"state" tfsdk:"-"` + MarkedForDeletion *MeshTenantLifecycleAction `json:"markedForDeletion" tfsdk:"-"` +} + +type MeshTenantLifecycleAction struct { + Timestamp string `json:"timestamp" tfsdk:"-"` } type MeshTenantV4Create struct { @@ -118,7 +138,14 @@ func (tenant *MeshTenantV4) CreationSuccessful() (done bool, err error) { } func (tenant *MeshTenantV4) DeletionSuccessful() (done bool, err error) { - return tenant == nil, nil + return tenant == nil || tenant.Status.Lifecycle.State == TenantLifecycleStateDeleted, nil +} + +func (tenant *MeshTenantV4) DeletionState() string { + if tenant == nil { + return tenantNotObserved + } + return tenantDeletionState(tenant.Status.Lifecycle) } type MeshTenant struct { @@ -155,6 +182,7 @@ type MeshTenantStatus struct { // only the values requested at create (create-only); the effective quotas here can differ once // landing-zone defaults are merged in or an operator adjusts them, so drift is tracked against these. AppliedQuotas map[string]AppliedQuotaValue `json:"appliedQuotas" tfsdk:"applied_quotas"` + Lifecycle MeshTenantLifecycle `json:"lifecycle" tfsdk:"-"` } // MeshTenantQuota is the {key, value} element of the deprecated list-form spec.quotas, superseded by @@ -262,5 +290,30 @@ func (tenant *MeshTenant) CreationSuccessful() (done bool, err error) { } func (tenant *MeshTenant) DeletionSuccessful() (done bool, err error) { - return tenant == nil, nil + return tenant == nil || tenant.Status.Lifecycle.State == TenantLifecycleStateDeleted, nil +} + +func (tenant *MeshTenant) DeletionState() string { + if tenant == nil { + return tenantNotObserved + } + return tenantDeletionState(tenant.Status.Lifecycle) +} + +const tenantNotObserved = "no successful read after the delete request" + +func tenantDeletionState(lifecycle MeshTenantLifecycle) string { + switch { + case lifecycle.State == TenantLifecycleStateDeleted: + return "DELETED" + case lifecycle.State == TenantLifecycleStateMarkedForDeletion && lifecycle.MarkedForDeletion != nil: + return fmt.Sprintf( + "MARKED_FOR_DELETION since %s, awaiting deletion approval, cleanup of the tenant's resources, or the platform deletion the replicator confirms", + lifecycle.MarkedForDeletion.Timestamp, + ) + case lifecycle.State == TenantLifecycleStateMarkedForDeletion: + return "MARKED_FOR_DELETION, awaiting deletion approval, cleanup of the tenant's resources, or the platform deletion the replicator confirms" + default: + return fmt.Sprintf("%s, meshStack accepted the delete request but has not acted on it", lifecycle.State) + } } diff --git a/tenant_v4_test.go b/tenant_v4_test.go new file mode 100644 index 0000000..e94c11a --- /dev/null +++ b/tenant_v4_test.go @@ -0,0 +1,131 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMeshTenant_DeletionSuccessful(t *testing.T) { + tests := []struct { + name string + tenant *MeshTenant + wantDone bool + }{ + { + name: "nil (404 — tenant purged)", + tenant: nil, + wantDone: true, + }, + { + name: "lifecycle DELETED (deletion completed, tenant still returned)", + tenant: &MeshTenant{Status: MeshTenantStatus{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateDeleted}, + }}, + wantDone: true, + }, + { + name: "lifecycle MARKED_FOR_DELETION (deletion still running)", + tenant: &MeshTenant{Status: MeshTenantStatus{ + Lifecycle: MeshTenantLifecycle{ + State: TenantLifecycleStateMarkedForDeletion, + MarkedForDeletion: &MeshTenantLifecycleAction{Timestamp: "2026-07-30T16:14:14Z"}, + }, + }}, + wantDone: false, + }, + { + name: "lifecycle ACTIVE", + tenant: &MeshTenant{Status: MeshTenantStatus{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateActive}, + }}, + wantDone: false, + }, + { + name: "no lifecycle reported", + tenant: &MeshTenant{Metadata: MeshTenantMetadata{Uuid: "test-uuid"}}, + wantDone: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + done, err := tt.tenant.DeletionSuccessful() + assert.Equal(t, tt.wantDone, done) + assert.NoError(t, err) + }) + } +} + +func TestMeshTenantV4_DeletionSuccessful(t *testing.T) { + tests := []struct { + name string + tenant *MeshTenantV4 + wantDone bool + }{ + { + name: "nil (404 — tenant purged)", + tenant: nil, + wantDone: true, + }, + { + name: "lifecycle DELETED (deletion completed, tenant still returned)", + tenant: &MeshTenantV4{Status: MeshTenantV4Status{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateDeleted}, + }}, + wantDone: true, + }, + { + name: "lifecycle MARKED_FOR_DELETION (deletion still running)", + tenant: &MeshTenantV4{Status: MeshTenantV4Status{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateMarkedForDeletion}, + }}, + wantDone: false, + }, + { + name: "lifecycle ACTIVE", + tenant: &MeshTenantV4{Status: MeshTenantV4Status{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateActive}, + }}, + wantDone: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + done, err := tt.tenant.DeletionSuccessful() + assert.Equal(t, tt.wantDone, done) + assert.NoError(t, err) + }) + } +} + +func TestTenantDeletionState(t *testing.T) { + assert.Equal(t, tenantNotObserved, (*MeshTenant)(nil).DeletionState()) + assert.Equal(t, tenantNotObserved, (*MeshTenantV4)(nil).DeletionState()) + + assert.Equal(t, "DELETED", + (&MeshTenant{Status: MeshTenantStatus{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateDeleted}, + }}).DeletionState(), + ) + assert.Contains(t, + (&MeshTenant{Status: MeshTenantStatus{Lifecycle: MeshTenantLifecycle{ + State: TenantLifecycleStateMarkedForDeletion, + MarkedForDeletion: &MeshTenantLifecycleAction{Timestamp: "2026-07-30T16:14:14Z"}, + }}}).DeletionState(), + "MARKED_FOR_DELETION since 2026-07-30T16:14:14Z", + ) + assert.Contains(t, + (&MeshTenantV4{Status: MeshTenantV4Status{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateMarkedForDeletion}, + }}).DeletionState(), + "MARKED_FOR_DELETION, awaiting", + ) + assert.Contains(t, + (&MeshTenant{Status: MeshTenantStatus{ + Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateActive}, + }}).DeletionState(), + "has not acted on it", + ) +} From 7d06765849e2bf33680355c25b5694594e65c85d Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 11 Aug 2026 13:04:16 +0200 Subject: [PATCH 184/200] feat!: reference parent building blocks by ref, and add the building block ref output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape `spec.parent_building_blocks` on `meshstack_building_block` from a set of `{buildingblock_uuid, definition_uuid}` objects into a plain set of `{kind, uuid}` building block refs. One ref replaces both fields because meshStack derives a parent's definition from the referenced building block. The attribute keeps its name: it is what the meshObject API calls the field, and naming it `parent_building_block_refs` would have made the provider the only place with a third spelling. `meshstack_building_block` also gains the computed `ref` output that feeds another block's `parent_building_blocks`, so the producing and consuming sides of a building-block reference land together. A state upgrader rewrites existing state, so an upgrade does not plan a replacement. `meshstack_building_blocks` reports parents in the same ref shape. The deprecated `meshstack_building_block_v2` resource and data source keep their flat `parent_building_blocks` unchanged: the meshObject API keeps accepting and returning the deprecated flat fields, so they map to and from the shared client DTO explicitly. The version floor stays at 2026.30.0. A parent is written as `kind`, `uuid` and the deprecated `buildingBlockUuid` twin carrying the identical value, and read from `uuid` falling back to `buildingBlockUuid` — so this release runs against today's backend as well as the flattened representation that follows it. That compatibility lives entirely in `MeshBuildingBlockV2Parent`'s JSON methods. BREAKING CHANGE: `meshstack_building_block`'s `spec.parent_building_blocks[*].buildingblock_uuid` and `spec.parent_building_blocks[*].definition_uuid` are removed; each element is now a `{kind, uuid}` ref. `meshstack_building_blocks` reports the same shape. Co-Authored-By: Claude Opus 5 (1M context) --- building_block_v2.go | 116 +++++++++++++++++++++++++++++++++++++- building_block_v2_test.go | 112 ++++++++++++++++++++++++++++++++++++ buildingblock.go | 2 + 3 files changed, 228 insertions(+), 2 deletions(-) diff --git a/building_block_v2.go b/building_block_v2.go index 1a2bf19..32171d8 100644 --- a/building_block_v2.go +++ b/building_block_v2.go @@ -53,8 +53,120 @@ type MeshBuildingBlockV2Spec struct { DisplayName string `json:"displayName" tfsdk:"display_name"` // Inputs as pointer MeshBuildingBlockInput to support mocking secret responses. - Inputs map[string]*MeshBuildingBlockInput `json:"inputs" tfsdk:"inputs"` - ParentBuildingBlocks types.Set[MeshBuildingBlockParent] `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` + Inputs map[string]*MeshBuildingBlockInput `json:"inputs" tfsdk:"inputs"` + ParentBuildingBlockRefs types.Set[UuidRef] `json:"parentBuildingBlockRefs" tfsdk:"parent_building_block_refs"` + + // ParentBuildingBlocks holds the deprecated parentBuildingBlocks field. MarshalJSON and + // UnmarshalJSON put it on the wire and take it off again, and the deprecated + // meshstack_building_block_v2 surfaces read it for the definition uuid they report. + ParentBuildingBlocks types.Set[MeshBuildingBlockV2Parent] `json:"-" tfsdk:"-"` +} + +// MeshBuildingBlockV2Parent is an entry of the deprecated parentBuildingBlocks field. +type MeshBuildingBlockV2Parent struct { + UuidRef + + // BuildingBlockUuid identifies the parent and always holds the same value as Uuid. + BuildingBlockUuid string `json:"buildingBlockUuid"` + // DefinitionUuid is the parent's building block definition. The backend derives it from the + // referenced block, so every response carries it and a request never does. + DefinitionUuid string `json:"definitionUuid,omitempty"` +} + +// UnmarshalJSON fills Uuid from the deprecated buildingBlockUuid, which is where a response carries +// the parent's identity. +func (p *MeshBuildingBlockV2Parent) UnmarshalJSON(data []byte) error { + type wire MeshBuildingBlockV2Parent + var target wire + if err := json.Unmarshal(data, &target); err != nil { + return err + } + + *p = MeshBuildingBlockV2Parent(target) + if p.Uuid == "" { + p.Uuid = p.BuildingBlockUuid + } + p.BuildingBlockUuid = p.Uuid + if p.Kind == "" { + p.Kind = MeshObjectKind.BuildingBlock + } + + return nil +} + +// MarshalJSON sends the parents under both field names: parentBuildingBlockRefs, and the deprecated +// parentBuildingBlocks for a backend that does not know the new field yet. A newer backend accepts +// both as long as they name the same building blocks, and an older one ignores the field it does not +// know, because the meshObject API does not reject unknown properties. +// +// Together with UnmarshalJSON this is the whole compatibility window. Once every backend still in use +// knows parentBuildingBlockRefs, both methods can go. +func (s MeshBuildingBlockV2Spec) MarshalJSON() ([]byte, error) { + type wire MeshBuildingBlockV2Spec + if len(s.ParentBuildingBlockRefs) == 0 { + s.ParentBuildingBlockRefs = parentRefsFromDeprecated(s.ParentBuildingBlocks) + } + + encoded, err := json.Marshal(wire(s)) + if err != nil { + return nil, err + } + + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + return nil, err + } + + // The deprecated entry sends only buildingBlockUuid: every backend in the supported range reads + // the parent from it, and the definition uuid is always derived from the referenced block. + parents := make([]struct { + BuildingBlockUuid string `json:"buildingBlockUuid"` + }, 0, len(s.ParentBuildingBlockRefs)) + for _, ref := range s.ParentBuildingBlockRefs { + parents = append(parents, struct { + BuildingBlockUuid string `json:"buildingBlockUuid"` + }{BuildingBlockUuid: ref.Uuid}) + } + if fields["parentBuildingBlocks"], err = json.Marshal(parents); err != nil { + return nil, err + } + + return json.Marshal(fields) +} + +func parentRefsFromDeprecated(parents types.Set[MeshBuildingBlockV2Parent]) types.Set[UuidRef] { + refs := make(types.Set[UuidRef], 0, len(parents)) + for _, parent := range parents { + refs = append(refs, UuidRef{Uuid: parent.Uuid, Kind: MeshObjectKind.BuildingBlock}) + } + return refs +} + +// UnmarshalJSON reads the parents from parentBuildingBlockRefs, or from the deprecated +// parentBuildingBlocks when a backend does not serve the new field yet. Terraform then sees the same +// elements against either backend and set hashing stays stable. +func (s *MeshBuildingBlockV2Spec) UnmarshalJSON(data []byte) error { + type wire MeshBuildingBlockV2Spec + var target struct { + wire + ParentBuildingBlocks types.Set[MeshBuildingBlockV2Parent] `json:"parentBuildingBlocks"` + } + if err := json.Unmarshal(data, &target); err != nil { + return err + } + + *s = MeshBuildingBlockV2Spec(target.wire) + s.ParentBuildingBlocks = target.ParentBuildingBlocks + if len(s.ParentBuildingBlockRefs) == 0 { + s.ParentBuildingBlockRefs = parentRefsFromDeprecated(s.ParentBuildingBlocks) + } + for i := range s.ParentBuildingBlockRefs { + if s.ParentBuildingBlockRefs[i].Kind == "" { + s.ParentBuildingBlockRefs[i].Kind = MeshObjectKind.BuildingBlock + } + } + + return nil } type MeshBuildingBlockInput struct { diff --git a/building_block_v2_test.go b/building_block_v2_test.go index d87f6cf..07440af 100644 --- a/building_block_v2_test.go +++ b/building_block_v2_test.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -9,6 +10,16 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) +const ( + testParentUuid = "11111111-1111-1111-1111-111111111111" + testParentDefinitionUuid = "22222222-2222-2222-2222-222222222222" + // testParentRef is the shape this provider sends for a parent in parentBuildingBlockRefs. + testParentRef = `{"kind": "meshBuildingBlock", "uuid": "` + testParentUuid + `"}` + // testDeprecatedParent is what the provider sends alongside it, for a backend that does not know + // parentBuildingBlockRefs yet. + testDeprecatedParent = `{"buildingBlockUuid": "` + testParentUuid + `"}` +) + func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { tests := []struct { name string @@ -193,3 +204,104 @@ func TestMeshBuildingBlockV2_CreateSuccessful(t *testing.T) { }) } } + +// TestMeshBuildingBlockV2Parent_UnmarshalJSON covers every response shape. Terraform has to see the +// same {kind, uuid} against every backend, so that set hashing and UseStateForUnknown stay stable. +func TestMeshBuildingBlockV2Parent_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + response string + wantDefinitionUuid string + }{ + { + // An older backend reports the parent inside a buildingBlockRef envelope, which this provider + // does not read, so only the deprecated field carries the uuid. + name: "enveloped response without a top-level uuid", + response: `{ + "buildingBlockRef": {"kind": "meshBuildingBlock", "uuid": "` + testParentUuid + `"}, + "buildingBlockUuid": "` + testParentUuid + `", + "definitionUuid": "` + testParentDefinitionUuid + `" + }`, + wantDefinitionUuid: testParentDefinitionUuid, + }, + { + name: "flattened response that also carries a top-level uuid", + response: `{ + "kind": "meshBuildingBlock", + "uuid": "` + testParentUuid + `", + "buildingBlockUuid": "` + testParentUuid + `", + "definitionUuid": "` + testParentDefinitionUuid + `" + }`, + wantDefinitionUuid: testParentDefinitionUuid, + }, + { + name: "flattened response once the deprecated fields are gone", + response: `{"kind": "meshBuildingBlock", "uuid": "` + testParentUuid + `"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var parent MeshBuildingBlockV2Parent + require.NoError(t, json.Unmarshal([]byte(tt.response), &parent)) + assert.Equal(t, MeshBuildingBlockV2Parent{ + UuidRef: UuidRef{Kind: MeshObjectKind.BuildingBlock, Uuid: testParentUuid}, + BuildingBlockUuid: testParentUuid, + DefinitionUuid: tt.wantDefinitionUuid, + }, parent) + }) + } +} + +// TestMeshBuildingBlockV2Spec_ParentsRoundTrip goes through the whole spec, so a change to the json +// tag of parentBuildingBlockRefs or to the Set element type is caught too. +func TestMeshBuildingBlockV2Spec_ParentsRoundTrip(t *testing.T) { + const response = `{ + "buildingBlockDefinitionVersionRef": {"kind": "meshBuildingBlockDefinitionVersion", "uuid": "33333333-3333-3333-3333-333333333333"}, + "targetRef": {"kind": "meshWorkspace", "name": "my-workspace"}, + "displayName": "child", + "inputs": {}, + "parentBuildingBlockRefs": [` + testParentRef + `], + "parentBuildingBlocks": [{"buildingBlockUuid": "` + testParentUuid + `", "definitionUuid": "` + testParentDefinitionUuid + `"}] + }` + + var spec MeshBuildingBlockV2Spec + require.NoError(t, json.Unmarshal([]byte(response), &spec)) + require.Len(t, spec.ParentBuildingBlockRefs, 1) + assert.Equal(t, UuidRef{Kind: MeshObjectKind.BuildingBlock, Uuid: testParentUuid}, spec.ParentBuildingBlockRefs[0]) + require.Len(t, spec.ParentBuildingBlocks, 1) + assert.Equal(t, testParentDefinitionUuid, spec.ParentBuildingBlocks[0].DefinitionUuid) + + assertSentUnderBothFieldNames(t, spec) +} + +// TestMeshBuildingBlockV2Spec_ParentsFromDeprecatedFieldOnly covers a backend that does not serve +// parentBuildingBlockRefs yet, and the deprecated meshstack_building_block_v2 surfaces, which fill +// only the deprecated field. +func TestMeshBuildingBlockV2Spec_ParentsFromDeprecatedFieldOnly(t *testing.T) { + const response = `{ + "buildingBlockDefinitionVersionRef": {"kind": "meshBuildingBlockDefinitionVersion", "uuid": "33333333-3333-3333-3333-333333333333"}, + "targetRef": {"kind": "meshWorkspace", "name": "my-workspace"}, + "displayName": "child", + "inputs": {}, + "parentBuildingBlocks": [{"buildingBlockUuid": "` + testParentUuid + `", "definitionUuid": "` + testParentDefinitionUuid + `"}] + }` + + var spec MeshBuildingBlockV2Spec + require.NoError(t, json.Unmarshal([]byte(response), &spec)) + require.Len(t, spec.ParentBuildingBlockRefs, 1) + assert.Equal(t, UuidRef{Kind: MeshObjectKind.BuildingBlock, Uuid: testParentUuid}, spec.ParentBuildingBlockRefs[0]) + + assertSentUnderBothFieldNames(t, spec) +} + +func assertSentUnderBothFieldNames(t *testing.T, spec MeshBuildingBlockV2Spec) { + t.Helper() + + out, err := json.Marshal(spec) + require.NoError(t, err) + var request map[string]json.RawMessage + require.NoError(t, json.Unmarshal(out, &request)) + assert.JSONEq(t, "["+testParentRef+"]", string(request["parentBuildingBlockRefs"])) + assert.JSONEq(t, "["+testDeprecatedParent+"]", string(request["parentBuildingBlocks"])) +} diff --git a/buildingblock.go b/buildingblock.go index 8ec9e5d..c8db231 100644 --- a/buildingblock.go +++ b/buildingblock.go @@ -46,6 +46,8 @@ type MeshBuildingBlockIO struct { ValueType string `json:"valueType" tfsdk:"value_type"` } +// MeshBuildingBlockParent is the v1 API's flat parent shape. The v2 API identifies a parent by +// reference instead — see MeshBuildingBlockV2Parent. type MeshBuildingBlockParent struct { BuildingBlockUuid string `json:"buildingBlockUuid" tfsdk:"buildingblock_uuid"` DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` From e44839a25af8e2a9e15d00a98d94aaaf71bcb8e3 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Mon, 10 Aug 2026 20:54:22 +0200 Subject: [PATCH 185/200] feat!: remove the deprecated spec.quotas from meshstack_tenant The meshTenant API deprecated the list-form spec.quotas in favour of the spec.requested_quotas map, which v0.24.3 added together with the computed status.applied_quotas. This drops the deprecated field from meshstack_tenant, its data source and meshstack_tenants, so the provider stops sending and modelling it. Only the unsuffixed, ref-based resource is touched. The deprecated meshstack_tenant_v4 keeps its own spec.quotas: it is removed wholesale once the meshTenant API goes GA. Existing state migrates automatically (schema version 1 -> 2): a quota recorded under spec.quotas is translated into spec.requested_quotas rather than dropped, so a configuration that restates the same quotas in the map form plans no change. That matters because a quota change on an existing tenant is rejected -- the meshTenant API cannot update one. The `moved` mover from meshstack_tenant_v4 translates the same way. Because this only stops using a field the API still accepts, it needs no newer meshStack version and the version floor stays at 2026.30.0. Issue: CU-86c0j0r7q --- tenant_v4.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tenant_v4.go b/tenant_v4.go index cfbb65a..314a1b7 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) @@ -168,8 +167,6 @@ type MeshTenantSpec struct { // {"limits.cpu": {"value": 4}}. The backend does not return it on read (it is a create-time input), // so the resource echoes the configured value from state. RequestedQuotas map[string]RequestQuotaValue `json:"requestedQuotas" tfsdk:"requested_quotas"` - // Deprecated: superseded by RequestedQuotas; retained so existing configurations keep working. - Quotas types.Set[MeshTenantQuota] `json:"quotas" tfsdk:"quotas"` } type MeshTenantStatus struct { @@ -221,14 +218,10 @@ type MeshTenantCreateMetadata struct { } type MeshTenantCreateSpec struct { - PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` - LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` - PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` - // RequestedQuotas is the preferred key->value form. Only one of the two quota fields should be - // set — the backend rejects a create that carries both with conflicting values. - RequestedQuotas map[string]RequestQuotaValue `json:"requestedQuotas,omitempty" tfsdk:"requested_quotas"` - // Deprecated: superseded by RequestedQuotas; retained so existing configurations keep working. - Quotas types.Set[MeshTenantQuota] `json:"quotas,omitempty" tfsdk:"quotas"` + PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` + LandingZoneRef *NamedRef `json:"landingZoneRef" tfsdk:"landing_zone_ref"` + PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` + RequestedQuotas map[string]RequestQuotaValue `json:"requestedQuotas,omitempty" tfsdk:"requested_quotas"` } type MeshTenantQuery struct { From 6a54f759fa21a02a8014ae6866c8e9d58537dbc2 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 12 Aug 2026 23:38:24 +0200 Subject: [PATCH 186/200] feat!: promote meshstack_tenant to the meshTenant v4 GA API Send the GA `v4` meshTenant media type instead of `v4-preview` from meshstack_tenant and meshstack_tenants, and drop the preview disclaimer from their documentation. This requires a meshStack backend that has promoted meshTenant v4 to GA; a backend that only serves the preview media type answers with HTTP 415. With the unsuffixed resource on the GA API, the deprecated meshstack_tenant_v4 resource and data source go away, together with their client (client.MeshTenantV4), models, mocks, builder, examples and docs, and their registration in the provider. The meshstack_tenant MoveState/moveFromV4 migration path goes with them, because the type it migrates from no longer exists -- so apply that `moved` block on v0.24.x before you upgrade. client/tenant_v4_test.go becomes client/tenant_v4_deletion_test.go, since the deletion helpers are all it still covers. The client package keeps the v4 in its file names, because it is the API version it talks to. The version floor moves to 2026.34.0, the first release that can still carry the backend flip: v2026.33.0 was tagged 2026-08-12 while the backend PR was open. docs/index.md is regenerated from the __MIN_MESHSTACK_VERSION__ placeholder in templates/index.md.tmpl rather than edited by hand. Issue: CU-86c0j0r7q --- client.go | 4 +- client_kind_test.go | 1 - internal/mesh_object_client.go | 2 +- tenant_v4.go | 132 +----------------- ...t_v4_test.go => tenant_v4_deletion_test.go | 46 +----- 5 files changed, 8 insertions(+), 177 deletions(-) rename tenant_v4_test.go => tenant_v4_deletion_test.go (66%) diff --git a/client.go b/client.go index e266727..de8e956 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.30.0") +var MinMeshStackVersion = version.MustParse("2026.32.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. @@ -37,7 +37,6 @@ type Client struct { ServiceInstance MeshServiceInstanceClient TagDefinition MeshTagDefinitionClient Tenant MeshTenantClient - TenantV4 MeshTenantV4Client Workspace MeshWorkspaceClient WorkspaceGroupBinding MeshWorkspaceGroupBindingClient WorkspaceUserBinding MeshWorkspaceUserBindingClient @@ -93,7 +92,6 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza ServiceInstance: newServiceInstanceClient(ctx, httpClient), TagDefinition: newTagDefinitionClient(ctx, httpClient), Tenant: newTenantClient(ctx, httpClient), - TenantV4: newTenantV4Client(ctx, httpClient), Workspace: newWorkspaceClient(ctx, httpClient), WorkspaceGroupBinding: newWorkspaceGroupBindingClient(ctx, httpClient), WorkspaceUserBinding: newWorkspaceUserBindingClient(ctx, httpClient), diff --git a/client_kind_test.go b/client_kind_test.go index 328acad..31a6b20 100644 --- a/client_kind_test.go +++ b/client_kind_test.go @@ -27,7 +27,6 @@ func TestKind(t *testing.T) { assert.Equal(t, internal.InferKind[MeshServiceInstance](), MeshObjectKind.ServiceInstance) assert.Equal(t, internal.InferKind[MeshTagDefinition](), MeshObjectKind.TagDefinition) assert.Equal(t, internal.InferKind[MeshTenant](), MeshObjectKind.Tenant) - assert.Equal(t, internal.InferKind[MeshTenantV4](), MeshObjectKind.Tenant) assert.Equal(t, internal.InferKind[MeshWorkspace](), MeshObjectKind.Workspace) assert.Equal(t, internal.InferKind[MeshWorkspaceGroupBinding](), MeshObjectKind.WorkspaceGroupBinding) assert.Equal(t, internal.InferKind[MeshWorkspaceUserBinding](), MeshObjectKind.WorkspaceUserBinding) diff --git a/internal/mesh_object_client.go b/internal/mesh_object_client.go index eec05f7..9cf8e5f 100644 --- a/internal/mesh_object_client.go +++ b/internal/mesh_object_client.go @@ -45,7 +45,7 @@ func NewMeshObjectClient[M any](ctx context.Context, httpClient HttpClient, apiV var versionSuffixRe = regexp.MustCompile(`V\d+$`) // InferKind infers the meshObject kind from a struct type name using the same convention -// as the meshObject API: MeshWorkspace → "meshWorkspace", MeshTenantV4 → "meshTenant". +// as the meshObject API: MeshWorkspace → "meshWorkspace", MeshBuildingBlockV2 → "meshBuildingBlock". // Version suffixes (V\d+) are stripped. // Tested when client.Kind is statically initialized. func InferKind[M any]() string { diff --git a/tenant_v4.go b/tenant_v4.go index 314a1b7..195f270 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -8,41 +8,6 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" ) -type MeshTenantV4 struct { - Metadata MeshTenantV4Metadata `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantV4Spec `json:"spec" tfsdk:"spec"` - Status MeshTenantV4Status `json:"status" tfsdk:"status"` -} - -type MeshTenantV4Metadata struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` - CreatedOn string `json:"createdOn" tfsdk:"created_on"` - MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` - DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` -} - -type MeshTenantV4Spec struct { - PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` - PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` - LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` - Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` -} - -type MeshTenantV4Status struct { - TenantName string `json:"tenantName" tfsdk:"tenant_name"` - PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` - PlatformWorkspaceIdentifier *string `json:"platformWorkspaceIdentifier" tfsdk:"platform_workspace_identifier"` - Tags map[string][]string `json:"tags" tfsdk:"tags"` - // AppliedQuotas are the effective quotas meshStack applied to the tenant as a key->value map, - // distinct from the create-only spec.quotas which carries only the requested values. Each value is a - // structured object (e.g. `{"limits.cpu": {"value": 4}}`) so the preview API can grow per-quota - // fields without a breaking change to the map shape. - AppliedQuotas map[string]AppliedQuotaValue `json:"appliedQuotas" tfsdk:"applied_quotas"` - Lifecycle MeshTenantLifecycle `json:"lifecycle" tfsdk:"-"` -} - type TenantLifecycleState string var ( @@ -61,92 +26,6 @@ type MeshTenantLifecycleAction struct { Timestamp string `json:"timestamp" tfsdk:"-"` } -type MeshTenantV4Create struct { - Metadata MeshTenantV4CreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshTenantV4CreateSpec `json:"spec" tfsdk:"spec"` -} - -type MeshTenantV4CreateMetadata struct { - OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"` - OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` -} - -type MeshTenantV4CreateSpec struct { - PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"` - LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"` - PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"` - Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` -} - -type MeshTenantV4Query struct { - Workspace string `json:"workspaceIdentifier"` - Project *string `json:"projectIdentifier"` - Platform *string `json:"platformIdentifier"` - PlatformType *string `json:"platformTypeIdentifier"` - LandingZone *string `json:"landingZoneIdentifier"` - PlatformTenant *string `json:"platformTenantId"` -} - -type MeshTenantV4Client interface { - Read(ctx context.Context, uuid string) (*MeshTenantV4, error) - ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) - List(ctx context.Context, query MeshTenantV4Query) ([]MeshTenantV4, error) - Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) - Delete(ctx context.Context, uuid string) error -} - -type meshTenantV4Client struct { - meshObject internal.MeshObjectClient[MeshTenantV4] -} - -func newTenantV4Client(ctx context.Context, httpClient internal.HttpClient) MeshTenantV4Client { - return meshTenantV4Client{internal.NewMeshObjectClient[MeshTenantV4](ctx, httpClient, "v4-preview")} -} - -func (c meshTenantV4Client) Read(ctx context.Context, uuid string) (*MeshTenantV4, error) { - return c.ReadFunc(uuid)(ctx) -} - -func (c meshTenantV4Client) ReadFunc(uuid string) func(ctx context.Context) (*MeshTenantV4, error) { - return func(ctx context.Context) (*MeshTenantV4, error) { - return c.meshObject.Get(ctx, uuid) - } -} - -func (c meshTenantV4Client) Create(ctx context.Context, tenant *MeshTenantV4Create) (*MeshTenantV4, error) { - return c.meshObject.Post(ctx, tenant) -} - -func (c meshTenantV4Client) List(ctx context.Context, query MeshTenantV4Query) ([]MeshTenantV4, error) { - return c.meshObject.List(ctx, internal.WithUrlQuery(query)) -} - -func (c meshTenantV4Client) Delete(ctx context.Context, uuid string) error { - return c.meshObject.Delete(ctx, uuid) -} - -func (tenant *MeshTenantV4) CreationSuccessful() (done bool, err error) { - switch { - case tenant == nil: - err = fmt.Errorf("tenant not found after creation") - case tenant.Spec.PlatformTenantId != nil && *tenant.Spec.PlatformTenantId != "": - // Creation is complete (platformTenantId is set and not empty) - done = true - } - return -} - -func (tenant *MeshTenantV4) DeletionSuccessful() (done bool, err error) { - return tenant == nil || tenant.Status.Lifecycle.State == TenantLifecycleStateDeleted, nil -} - -func (tenant *MeshTenantV4) DeletionState() string { - if tenant == nil { - return tenantNotObserved - } - return tenantDeletionState(tenant.Status.Lifecycle) -} - type MeshTenant struct { Metadata MeshTenantMetadata `json:"metadata" tfsdk:"metadata"` Spec MeshTenantSpec `json:"spec" tfsdk:"spec"` @@ -182,17 +61,16 @@ type MeshTenantStatus struct { Lifecycle MeshTenantLifecycle `json:"lifecycle" tfsdk:"-"` } -// MeshTenantQuota is the {key, value} element of the deprecated list-form spec.quotas, superseded by -// the requested_quotas / applied_quotas maps. It is still the quota shape of the deprecated -// meshstack_tenant_v4 resource, so it carries no godoc deprecation marker. +// MeshTenantQuota is the {key, value} element of the removed list-form spec.quotas. The schema version 1 +// prior state still declares that attribute, so the state upgrader needs this shape to read it. type MeshTenantQuota struct { Key string `json:"key" tfsdk:"key"` Value int64 `json:"value" tfsdk:"value"` } // RequestQuotaValue is a tenant quota value as requested at create time. The scalar is wrapped in an -// object (rather than a bare number) so the v4 preview API can grow per-quota fields — e.g. a unit — -// without a breaking change to the requested_quotas map shape. +// object (rather than a bare number) so the v4 API can grow per-quota fields — e.g. a unit — without a +// breaking change to the requested_quotas map shape. // // Its shape is identical to AppliedQuotaValue, deliberately so: the resource must echo the configured // request in spec while reading effective values from status, and separate types turn mixing the two @@ -246,7 +124,7 @@ type meshTenantClient struct { } func newTenantClient(ctx context.Context, httpClient internal.HttpClient) MeshTenantClient { - return meshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v4-preview")} + return meshTenantClient{internal.NewMeshObjectClient[MeshTenant](ctx, httpClient, "v4")} } func (c meshTenantClient) Read(ctx context.Context, uuid string) (*MeshTenant, error) { diff --git a/tenant_v4_test.go b/tenant_v4_deletion_test.go similarity index 66% rename from tenant_v4_test.go rename to tenant_v4_deletion_test.go index e94c11a..25e60bc 100644 --- a/tenant_v4_test.go +++ b/tenant_v4_deletion_test.go @@ -57,52 +57,8 @@ func TestMeshTenant_DeletionSuccessful(t *testing.T) { } } -func TestMeshTenantV4_DeletionSuccessful(t *testing.T) { - tests := []struct { - name string - tenant *MeshTenantV4 - wantDone bool - }{ - { - name: "nil (404 — tenant purged)", - tenant: nil, - wantDone: true, - }, - { - name: "lifecycle DELETED (deletion completed, tenant still returned)", - tenant: &MeshTenantV4{Status: MeshTenantV4Status{ - Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateDeleted}, - }}, - wantDone: true, - }, - { - name: "lifecycle MARKED_FOR_DELETION (deletion still running)", - tenant: &MeshTenantV4{Status: MeshTenantV4Status{ - Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateMarkedForDeletion}, - }}, - wantDone: false, - }, - { - name: "lifecycle ACTIVE", - tenant: &MeshTenantV4{Status: MeshTenantV4Status{ - Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateActive}, - }}, - wantDone: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - done, err := tt.tenant.DeletionSuccessful() - assert.Equal(t, tt.wantDone, done) - assert.NoError(t, err) - }) - } -} - func TestTenantDeletionState(t *testing.T) { assert.Equal(t, tenantNotObserved, (*MeshTenant)(nil).DeletionState()) - assert.Equal(t, tenantNotObserved, (*MeshTenantV4)(nil).DeletionState()) assert.Equal(t, "DELETED", (&MeshTenant{Status: MeshTenantStatus{ @@ -117,7 +73,7 @@ func TestTenantDeletionState(t *testing.T) { "MARKED_FOR_DELETION since 2026-07-30T16:14:14Z", ) assert.Contains(t, - (&MeshTenantV4{Status: MeshTenantV4Status{ + (&MeshTenant{Status: MeshTenantStatus{ Lifecycle: MeshTenantLifecycle{State: TenantLifecycleStateMarkedForDeletion}, }}).DeletionState(), "MARKED_FOR_DELETION, awaiting", From b605a9d5fe4d55b59948acf8fa9511c516f6d249 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 18 Aug 2026 13:34:48 +0200 Subject: [PATCH 187/200] feat: bootstrap the meshStack CLI repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up the repository so the meshStack API client has somewhere to move to: a Taskfile, a Go-only CI workflow, a Nix dev shell and a cobra root command that prints help and nothing else yet. The dependency policy is the part worth reading. This repository is allowed one external dependency, cobra, and only cmd/ may use it; everything else stays on the standard library, with testify permitted in tests. The reason is that the Terraform provider will import client/ and pkg/login, so anything added here lands in the provider's dependency tree and in the public checksum database. depguard in .golangci.yml enforces that per directory. Two things about those depguard patterns. A rule that matches no file is a silent no-op, so each rule was verified by compiling a file that violates it. That is how the pattern bug surfaced: '**/client/**/*.go' matches files in subdirectories of client/ only, never files directly inside it, which would have left most of the client unguarded. Both patterns are therefore listed. The binary is meshstack while the module is meshstack-cli, and the layout is what reconciles them: the main package sits in cmd/meshstack, so 'go build' and 'go install' name the binary after that directory. Neither needs -o, and there is no main.go at the repository root, which would have named the binary after the module instead. cmd/meshstack holds main() and the root command together and is the one directory under cmd/ that is not a subcommand. The root command sets RunE even though it has no work to do, because cobra's help template skips the usage block entirely while a command is neither runnable nor a parent of subcommands — without it, 'meshstack --help' printed one line and no flags at all. Args is NoArgs so an unknown argument fails instead of silently printing help. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 72 +++++++ .gitignore | 12 ++ .golangci.yml | 106 +++++++++++ AGENTS.md | 110 +++++++++++ CLAUDE.md | 1 + LICENSE | 375 +++++++++++++++++++++++++++++++++++++ README.md | 24 +++ Taskfile.yml | 34 ++++ cmd/meshstack/main.go | 49 +++++ flake.nix | 49 +++++ go.mod | 10 + go.sum | 10 + 12 files changed, 852 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Taskfile.yml create mode 100644 cmd/meshstack/main.go create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f9659ee --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,72 @@ +# meshStack CLI build, lint and test workflow. +name: Tests + +on: + pull_request: + paths-ignore: + - 'README.md' + push: + branches: + - main + paths-ignore: + - 'README.md' + +# Testing only needs permissions to read the repository contents. +permissions: + contents: read + +# Cancel superseded runs on the same ref. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Go Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod tidy + - run: go build -v ./... + - name: git diff + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after 'go mod tidy'. Run 'go mod tidy' command and commit."; exit 1) + + golangci: + needs: [ build ] + name: Go Lint and Format Check + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read # Required for only-new-issues on PRs + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 + with: + version: latest + only-new-issues: true # Show only issues in changed code on PRs + - name: Suggest fix command on failure + if: failure() + run: | + echo "::error::Linting or formatting issues detected. Run 'task lint -- --fix' locally to automatically fix these issues, then commit the changes." + + test: + needs: [ build ] + name: Go Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go test -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20d33a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Binary produced by 'task build' +/meshstack + +# Go environment created by the Nix dev shell (flake.nix shellHook) +/.nix-go/ + +# Local meshStack credentials, read by the Taskfile's dotenv +.env + +# Editor and IDE directories +.vscode/ +.idea/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2bfefe4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,106 @@ +# Visit https://golangci-lint.run/ for usage documentation +# and information on other useful linters. +# +# Kept deliberately close to the meshStack Terraform provider's configuration, so +# that code moving between the two repositories does not trip a different linter set. +version: "2" +issues: + max-same-issues: 0 + +formatters: + enable: + - gci + - gofmt + settings: + gci: + sections: + - standard # Go standard library + - default # All other external dependencies + - localmodule # This repository's modules + +linters: + default: none + enable: + - depguard + - durationcheck + - errcheck + - copyloopvar + - forcetypeassert + - godot + - ineffassign + - makezero + - misspell + - nilerr + - predeclared + - staticcheck + - usetesting + - unconvert + - unparam + - unused + - govet + - testifylint + - thelper + settings: + # This repository is allowed exactly one external dependency, cobra, and only the + # cmd/ tree may use it. Everything else stays on the standard library, with testify + # permitted in tests. The rules below are what enforces that; read them as the + # dependency policy rather than as lint configuration. + depguard: + rules: + # client/ must stay free of external dependencies. The meshStack Terraform + # provider consumes this package, so anything added here lands in the + # provider's dependency tree as well. + client: + files: + # Both patterns are needed: '**/dir/**/*.go' only matches files in + # subdirectories of dir, never files directly inside it. + - "**/client/*.go" + - "**/client/**/*.go" + - "!$test" + list-mode: strict + allow: + - $gostd + - github.com/meshcloud/meshstack-cli/client + + # pkg/ holds logic reusable outside a CLI process — the Terraform provider + # imports pkg/login — so cobra must not reach it. + pkg: + files: + - "**/pkg/*.go" + - "**/pkg/**/*.go" + - "!$test" + list-mode: strict + deny: + - pkg: github.com/spf13/cobra + desc: cobra belongs in cmd/; pkg/ is also consumed by the Terraform provider + allow: + - $gostd + - github.com/meshcloud/meshstack-cli/client + - github.com/meshcloud/meshstack-cli/pkg + + # cmd/ builds the command tree, and is the only place cobra is used. + cmd: + files: + - "**/cmd/*.go" + - "**/cmd/**/*.go" + - "!$test" + list-mode: strict + deny: + - pkg: log # as $gostd is allowed + desc: Write user-facing output through the command's own streams + allow: + - $gostd + - github.com/meshcloud/meshstack-cli + - github.com/spf13/cobra + + # Tests may additionally use testify, which is what the client's moved tests + # are written against. + tests: + files: + - "$test" + list-mode: strict + allow: + - $gostd + - github.com/meshcloud/meshstack-cli + - github.com/spf13/cobra + - github.com/stretchr/testify diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4e52fa1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,110 @@ +# AGENTS.md — meshStack CLI + + +You are an expert Go engineer working on the meshStack CLI: the `meshstack` binary, and the Go +client for the meshStack API that the +[meshStack Terraform provider](https://github.com/meshcloud/terraform-provider-meshstack) imports as +a library. This file is the always-on source of truth for both AI agents and humans. + + +> **This repository is public.** Write everything here so an external contributor with no meshcloud +> access can follow it. Tag meshcloud-internal shortcuts clearly as internal, and never let +> understanding a rule *depend* on them. + +## Naming + +- **`meshstack`** — the binary, so every invocation reads `meshstack buildingblock list`. +- **meshStack CLI** — the product name, used in prose and docs. +- `github.com/meshcloud/meshstack-cli` — the repository and Go module. + +The binary gets its name from its directory, `cmd/meshstack`, which is why there is no `-o` flag +anywhere: `go build ./cmd/meshstack` and +`go install github.com/meshcloud/meshstack-cli/cmd/meshstack@latest` both produce `meshstack`. **Do +not add a `main.go` at the repository root**; that would name the binary after the module and bring +the flag back. + +## Package layout + +| Path | Holds | +|---|---| +| `cmd/meshstack/` | `package main`: `main()` and the root command. The only main package. | +| `cmd//` | One package per subcommand of the cobra command tree. | +| `pkg/` | Logic that does not need a CLI process, and that the Terraform provider can import. | +| `client/` | The meshStack API client. Path-identical to the provider's former `client/`. | + + +In `cmd/`, **the package name is the subcommand and the file name is the leaf command**: +`cmd/buildingblock/list.go` holds `meshstack buildingblock list`. Each package exports a `New` +function returning its `*cobra.Command`, and `cmd/meshstack` wires children in with `AddCommand`. + +`cmd/meshstack` is the one exception to that rule, and is not a subcommand: it is the binary's +`package main`, holding `main()` and the root command together. + +Register commands **explicitly in `cmd/meshstack`, never from `init()`**, so the command tree reads in +one place and a command cannot appear in the binary just because its package was imported for some +other reason. + + +## Dependency policy + +The CLI is allowed **exactly one external dependency, cobra**, and only `cmd/` may use it. Everything +else is standard library, with `testify` permitted in tests. + +This is not austerity for its own sake. The Terraform provider imports `client/` and `pkg/login`, so +every dependency added here lands in the provider's dependency tree, and from there in the public +checksum database. `depguard` in `.golangci.yml` enforces the boundaries per directory; read those +rules as the policy. Widening them is a deliberate decision, not a lint fix. + + +`client/` keeps the import path it had in the Terraform provider, so moving code between the two +repositories stays a plain path rewrite. + +The login exchange lives in `client/internal/auth.go`, which posts to `/api/login`, caches the access +token and refreshes it before expiry. Go's internal rule keeps that code inside `client/`; reach it +through `client.NewApiKeyAuthorization`. Do **not** write a second login exchange elsewhere — a +hand-rolled one gets a static token and starts returning 401 once it expires. + +`pkg/login` owns credential resolution only: it reads the environment and returns a +`client.Authorization`. It is also the single place that constructs one, which is where token caching +on disk will hook in later. + + +## Always-on rules + + + +- **Lean comments.** A comment earns its place only by saying what the code cannot — the *why*, a + trade-off, a non-obvious constraint. Don't restate what a name, type or signature already conveys; + prefer one sharp line over a paragraph. +- **Lint only via `task lint`** (golangci-lint, which also enforces gci import ordering and gofmt). + It already runs `govet`, so **do not run `go vet` separately**. Auto-fix with `task lint -- --fix`. +- **Conventional Commits** for messages (`feat:`, `fix:`, `docs:`, `chore:`, `feat!:` for breaking). +- **Stress-test a plan before writing code.** For any non-trivial change, walk each branch of the + decision tree and settle every open question with a recommended answer first. Catching a wrong turn + at the plan stage is far cheaper than after the code and tests exist. (*meshcloud-internal*: the + `grill-me` skill in `meshfed-release/.agents/skills/`.) + + + +## Commands + +Everything runs through the Taskfile, inside `nix develop`: + +```shell +task build # ./meshstack +task test # go test ./... +task lint # golangci-lint run +task tidy # go mod tidy +``` + +The Go version is pinned in `go.mod` and in `flake.nix`; **keep them in lock-step when bumping**, and +keep them aligned with the Terraform provider, which consumes this module. + +## Authentication + +`MESHSTACK_ENDPOINT`, `MESHSTACK_API_KEY` and `MESHSTACK_API_SECRET`, with `MESHSTACK_API_TOKEN` as +an alternative to the key and secret pair. The names are exported as consts from `pkg/login`, so the +provider and the CLI share one definition — use those consts rather than the string literals. The +Taskfile reads a git-ignored `.env` for local runs. + +`MESHSTACK_SKIP_VERSION_CHECK=true` skips the minimum backend version check in `client/client.go`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2d7be93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,375 @@ +Copyright (c) 2026 meshcloud GmbH + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..845c1ca --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# meshStack CLI + +`meshstack` is the command line interface for [meshStack](https://www.meshcloud.io/). + +## Install + +```shell +go install github.com/meshcloud/meshstack-cli/cmd/meshstack@latest +``` + +## Development + +The Nix dev shell provides Go, `golangci-lint` and `task`: + +```shell +nix develop +task build # ./meshstack +task test # go test ./... +task lint # golangci-lint run, add -- --fix to apply fixes +``` + +The Go version is pinned in `go.mod` and in `flake.nix`, and is kept in lock-step with the +[meshStack Terraform provider](https://github.com/meshcloud/terraform-provider-meshstack), which +imports this repository's client package. diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..0b377e1 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,34 @@ +version: '3' + +dotenv: ['.env'] + +tasks: + build: + desc: Build the meshstack binary + cmds: + - go build {{.CLI_ARGS}} ./cmd/meshstack + + install: + desc: Install the meshstack binary into GOBIN + cmds: + - go install {{.CLI_ARGS}} ./cmd/meshstack + + test: + desc: Run unit tests + cmds: + - go test ./... {{.CLI_ARGS}} + + lint: + desc: Run golangci-lint + cmds: + - golangci-lint run {{.CLI_ARGS}} + + tidy: + desc: Tidy go.mod and go.sum + cmds: + - go mod tidy + + clean: + desc: Remove build artifacts + cmds: + - rm -f meshstack diff --git a/cmd/meshstack/main.go b/cmd/meshstack/main.go new file mode 100644 index 0000000..943b695 --- /dev/null +++ b/cmd/meshstack/main.go @@ -0,0 +1,49 @@ +// Command meshstack is the command line interface for meshStack. +// +// This package holds the root command. Every other package under cmd/ follows one +// rule: the package name is the subcommand and the file name is the leaf command, +// so cmd/buildingblock/list.go holds `meshstack buildingblock list`. Each of those +// packages exports a New function returning its *cobra.Command, and this package +// wires them in with AddCommand. Registration is explicit rather than done from +// init(), so the whole command tree can be read in one place and a command cannot +// appear in the binary just because its package was imported for another reason. +// +// The directory is named meshstack, not meshstack-cli, because `go build` and +// `go install` name the binary after it. +package main + +import ( + "os" + + "github.com/spf13/cobra" +) + +// Version identifies this build. A release overrides it with +// -ldflags "-X main.Version=", and it also identifies the CLI to the meshStack +// API through the client's user agent. +var Version = "dev" + +func main() { + if err := newRootCommand().Execute(); err != nil { + // cobra has already written the error to stderr. + os.Exit(1) + } +} + +func newRootCommand() *cobra.Command { + return &cobra.Command{ + Use: "meshstack", + Short: "Command line interface for meshStack", + // Running `meshstack` on its own prints the help text. RunE also has to be set + // for cobra to render the usage block at all: its help template skips usage + // while the command is neither runnable nor a parent of subcommands. + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + Version: Version, + // A command that fails prints its error, not the whole help text. The user asks + // for help explicitly. + SilenceUsage: true, + } +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2d71b36 --- /dev/null +++ b/flake.nix @@ -0,0 +1,49 @@ +{ + description = "meshStack CLI"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import nixpkgs { inherit system; }; + }); + in + { + devShells = forEachSupportedSystem ({ pkgs }: { + default = pkgs.mkShell { + packages = with pkgs; [ + # go 1.26 (pinned, in lock-step with go.mod — and with the meshStack + # Terraform provider, which consumes this repository's client package) + go_1_26 + + # goimports, godoc, etc. + gotools + + # https://github.com/golangci/golangci-lint + golangci-lint + + # https://taskfile.dev + go-task + ]; + + shellHook = '' + # Explicitly set GOROOT to Nix-installed Go + export GOROOT="${pkgs.go_1_26}/share/go" + + # Isolate Go environment from system + export GOPATH="$PWD/.nix-go" + export GOCACHE="$PWD/.nix-go/cache" + export GOMODCACHE="$PWD/.nix-go/mod" + export GOBIN="$PWD/.nix-go/bin" + export PATH="$GOBIN:$PATH" + + mkdir -p "$GOPATH" "$GOCACHE" "$GOMODCACHE" "$GOBIN" + ''; + }; + }); + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a149f44 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/meshcloud/meshstack-cli + +go 1.26 // keep flake.nix's pinned Go (go_1_26 + GOROOT) in lock-step when bumping + +require github.com/spf13/cobra v1.10.2 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6ee3e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 6b4fee957a61224eee7d99638f7d481671dce1b3 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 18 Aug 2026 16:14:16 +0200 Subject: [PATCH 188/200] fix: honor MESHSTACK_SKIP_VERSION_CHECK before requesting /mesh/info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The opt-out was evaluated inside the version-mismatch branch, so it was only reachable once GET /mesh/info had already succeeded. Setting the flag therefore never skipped the request — it only suppressed a version mismatch. /mesh/info is a GET on the retrying client, so an unavailable meshStack made every provider configure block for the client's full retry budget (~4 minutes) and then fail, with no way to opt out. Move the check to the top of checkMeshVersion so the flag short-circuits before the request is built. Co-Authored-By: Claude Opus 5 --- client.go | 10 +++++++--- client_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 client_test.go diff --git a/client.go b/client.go index de8e956..69b8a20 100644 --- a/client.go +++ b/client.go @@ -99,6 +99,13 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza } func checkMeshVersion(ctx context.Context, httpClient internal.HttpClient) error { + // Skip before the request, not just before the comparison: /mesh/info is a GET on the retrying + // client, so an unavailable backend blocks provider configuration for the whole retry budget + // (~4 minutes) and then fails it. Opting out of the check has to opt out of that too. + if os.Getenv("MESHSTACK_SKIP_VERSION_CHECK") == "true" { + return nil + } + type MeshInfo struct { Version version.Version `json:"version"` } @@ -107,9 +114,6 @@ func checkMeshVersion(ctx context.Context, httpClient internal.HttpClient) error if meshInfo, err := internal.DoRequest[MeshInfo](ctx, httpClient, "GET", meshInfoEndpoint); err != nil { return fmt.Errorf("failed to retrieve meshStack version information from %s endpoint: %w", meshInfoEndpoint, err) } else if meshInfo.Version.Less(MinMeshStackVersion) { - if os.Getenv("MESHSTACK_SKIP_VERSION_CHECK") == "true" { - return nil - } return fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) } return nil diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..241f94e --- /dev/null +++ b/client_test.go @@ -0,0 +1,44 @@ +package client + +import ( + "errors" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" +) + +type erroringRoundTripper struct{ calls int } + +func (rt *erroringRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { + rt.calls++ + return nil, errors.New("no server is available to handle this request") +} + +func TestCheckMeshVersion_SkipsRequestWhenOptedOut(t *testing.T) { + newUnreachableClient := func() (internal.HttpClient, *erroringRoundTripper) { + transport := new(erroringRoundTripper) + httpClient := internal.NewHttpClient(&url.URL{Scheme: "https", Host: "meshstack.invalid"}, "test-agent", nil) + httpClient.Transport = transport + return httpClient, transport + } + + t.Run("MESHSTACK_SKIP_VERSION_CHECK=true skips the /mesh/info request entirely", func(t *testing.T) { + t.Setenv("MESHSTACK_SKIP_VERSION_CHECK", "true") + httpClient, transport := newUnreachableClient() + require.NoError(t, checkMeshVersion(t.Context(), httpClient)) + assert.Zero(t, transport.calls, "opting out of the version check must not send a request that can block on retries") + }) + + t.Run("without the opt-out an unreachable /mesh/info fails", func(t *testing.T) { + t.Setenv("MESHSTACK_SKIP_VERSION_CHECK", "") + httpClient, transport := newUnreachableClient() + err := checkMeshVersion(t.Context(), httpClient) + require.ErrorContains(t, err, "failed to retrieve meshStack version information") + assert.Equal(t, 1, transport.calls) + }) +} From cc1894e03d158c29352cd31ad332429d73d05206 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 19 Aug 2026 09:39:42 +0200 Subject: [PATCH 189/200] fix: tolerate unknown spec values and ref-shaped meshTenant v4 reads A building block plan converted spec out of tfsdk.Plan, which fails whenever an attribute is wired to a resource the same plan creates or replaces. ModifyPlan now walks the planned spec for unknowns and schedules a run instead, matching what it already did for an unknown definition version ref. meshstack_tenant_v4 also lost spec.platform_identifier and spec.landing_zone_identifier against a meshStack that serves meshTenant v4 in its ref shape. Both force replacement, so a refresh planned the recreation of a live tenant. They are recovered from spec.landingZoneRef.name and from status.tenantName, without relying on the dropped flat identifiers. Together these unblock destroying a building block composition created before the backend moved meshTenant v4 to refs. --- tenant_v4.go | 53 +++++++++++++++++++++++++ tenant_v4_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/tenant_v4.go b/tenant_v4.go index 314a1b7..ccc7f89 100644 --- a/tenant_v4.go +++ b/tenant_v4.go @@ -2,7 +2,9 @@ package client import ( "context" + "encoding/json" "fmt" + "strings" "github.com/meshcloud/terraform-provider-meshstack/client/internal" "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" @@ -30,6 +32,57 @@ type MeshTenantV4Spec struct { Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"` } +// UnmarshalJSON fills the identifier-shaped spec from a ref-shaped meshTenant v4 payload. +// +// meshStack replaced spec.platformIdentifier and spec.landingZoneIdentifier with platformRef and +// landingZoneRef, which this deprecated identifier-based resource has no attribute for. Both fields +// would otherwise read back empty, and because both force replacement, a refresh makes Terraform +// plan the destruction and recreation of a live tenant. meshstack_tenant is the ref-based +// replacement; this keeps meshstack_tenant_v4 readable until a configuration has migrated. +// +// The landing zone comes straight off landingZoneRef.name. The platform identifier is not on the +// wire at all, so it is recovered from status.tenantName, which meshStack composes as +// "..". +func (t *MeshTenantV4) UnmarshalJSON(data []byte) error { + type wire MeshTenantV4 + var target wire + if err := json.Unmarshal(data, &target); err != nil { + return err + } + *t = MeshTenantV4(target) + + var refs struct { + Spec struct { + PlatformRef *UuidRef `json:"platformRef"` + LandingZoneRef *NamedRef `json:"landingZoneRef"` + } `json:"spec"` + } + if err := json.Unmarshal(data, &refs); err != nil { + return err + } + + if t.Spec.LandingZoneIdentifier == nil && refs.Spec.LandingZoneRef != nil { + t.Spec.LandingZoneIdentifier = &refs.Spec.LandingZoneRef.Name + } + if t.Spec.PlatformIdentifier == "" { + t.Spec.PlatformIdentifier = platformIdentifierFromTenantName( + t.Status.TenantName, t.Metadata.OwnedByWorkspace, t.Metadata.OwnedByProject) + } + + return nil +} + +// platformIdentifierFromTenantName strips the ".." prefix off a tenant name. +// It returns "" when the name does not carry that prefix, which leaves the caller with the same +// empty value it would have had without this recovery rather than a wrong platform identifier. +func platformIdentifierFromTenantName(tenantName, workspace, project string) string { + prefix := workspace + "." + project + "." + if !strings.HasPrefix(tenantName, prefix) { + return "" + } + return strings.TrimPrefix(tenantName, prefix) +} + type MeshTenantV4Status struct { TenantName string `json:"tenantName" tfsdk:"tenant_name"` PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"` diff --git a/tenant_v4_test.go b/tenant_v4_test.go index e94c11a..4aa2fd7 100644 --- a/tenant_v4_test.go +++ b/tenant_v4_test.go @@ -1,9 +1,11 @@ package client import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMeshTenant_DeletionSuccessful(t *testing.T) { @@ -129,3 +131,99 @@ func TestTenantDeletionState(t *testing.T) { "has not acted on it", ) } + +// TestMeshTenantV4_UnmarshalJSON_RefShapedSpec covers the meshTenant v4 payload after meshStack +// dropped the deprecated flat identifiers. Both attributes force replacement on +// meshstack_tenant_v4, so reading them back empty would make a refresh plan the recreation of a +// live tenant. +func TestMeshTenantV4_UnmarshalJSON_RefShapedSpec(t *testing.T) { + const refShaped = `{ + "metadata": { + "uuid": "124b09ec-63b8-452e-a837-44afb382d5bd", + "ownedByWorkspace": "smoke-test", + "ownedByProject": "smoke-test-20260708163151-dev" + }, + "spec": { + "platformRef": { "uuid": "403af12b-fbd5-41f4-aad2-b8c5311bc651", "kind": "meshPlatform" }, + "landingZoneRef": { "name": "smoketest-ske-dev", "kind": "meshLandingZone" }, + "platformTenantId": "smoke-test-smoke-test-20260708163151-dev" + }, + "status": { + "tenantName": "smoke-test.smoke-test-20260708163151-dev.smoke-test-ske-platform.global" + } + }` + + var tenant MeshTenantV4 + require.NoError(t, json.Unmarshal([]byte(refShaped), &tenant)) + + assert.Equal(t, "smoke-test-ske-platform.global", tenant.Spec.PlatformIdentifier, + "platform identifier is recovered from status.tenantName") + require.NotNil(t, tenant.Spec.LandingZoneIdentifier) + assert.Equal(t, "smoketest-ske-dev", *tenant.Spec.LandingZoneIdentifier, + "landing zone identifier is recovered from spec.landingZoneRef.name") +} + +func TestMeshTenantV4_UnmarshalJSON_KeepsFlatIdentifiersWhenPresent(t *testing.T) { + const flat = `{ + "metadata": { "ownedByWorkspace": "ws", "ownedByProject": "proj" }, + "spec": { + "platformIdentifier": "flat-platform.global", + "landingZoneIdentifier": "flat-lz", + "landingZoneRef": { "name": "ref-lz", "kind": "meshLandingZone" } + }, + "status": { "tenantName": "ws.proj.tenant-name-platform.global" } + }` + + var tenant MeshTenantV4 + require.NoError(t, json.Unmarshal([]byte(flat), &tenant)) + + assert.Equal(t, "flat-platform.global", tenant.Spec.PlatformIdentifier) + require.NotNil(t, tenant.Spec.LandingZoneIdentifier) + assert.Equal(t, "flat-lz", *tenant.Spec.LandingZoneIdentifier) +} + +func TestPlatformIdentifierFromTenantName(t *testing.T) { + tests := []struct { + name string + tenantName string + workspace string + project string + want string + }{ + { + name: "platform identifier contains dots", + tenantName: "ws.proj.platform-name.global", + workspace: "ws", + project: "proj", + want: "platform-name.global", + }, + { + name: "workspace and project contain dots", + tenantName: "my.ws.my.proj.platform.global", + workspace: "my.ws", + project: "my.proj", + want: "platform.global", + }, + { + // A name that does not carry the expected prefix yields "" rather than a wrong platform. + name: "prefix does not match", + tenantName: "other.tenant.platform.global", + workspace: "ws", + project: "proj", + want: "", + }, + { + name: "empty tenant name", + tenantName: "", + workspace: "ws", + project: "proj", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, platformIdentifierFromTenantName(tt.tenantName, tt.workspace, tt.project)) + }) + } +} From 5a27467bd1581c6ba331e7b11da6138b037c8c9b Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Thu, 13 Aug 2026 13:01:12 +0200 Subject: [PATCH 190/200] feat: manage a landing zone's restricted flag via spec.restricted --- client.go | 2 +- landingzone.go | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client.go b/client.go index 69b8a20..d6a6372 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/version" ) -var MinMeshStackVersion = version.MustParse("2026.32.0") +var MinMeshStackVersion = version.MustParse("2026.34.0") // HttpError represents an HTTP error response with status code. // This error is returned when an HTTP request fails with a non-2XX status code. diff --git a/landingzone.go b/landingzone.go index 259be05..33f4968 100644 --- a/landingzone.go +++ b/landingzone.go @@ -19,10 +19,15 @@ type MeshLandingZoneMetadata struct { } type MeshLandingZoneSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Description string `json:"description" tfsdk:"description"` - AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` - AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + Description string `json:"description" tfsdk:"description"` + AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` + AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` + // Nullable in the API: absent means "keep the stored value". The schema defaults this to false, + // so the provider always *sends* a value — the pointer is only needed when *reading* a state + // file that an older version of this Terraform provider wrote, at a time when this field did + // not exist yet; the attribute reads back as null there. + Restricted *bool `json:"restricted,omitempty" tfsdk:"restricted"` InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` PlatformProperties *MeshLandingZonePlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` From 7eb18816469762d42c5bd887740c54ab6d551524 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Fri, 14 Aug 2026 10:10:32 +0200 Subject: [PATCH 191/200] refactor: model landing zone spec.restricted as a plain bool Co-Authored-By: Claude Opus 5 --- landingzone.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/landingzone.go b/landingzone.go index 33f4968..346c48f 100644 --- a/landingzone.go +++ b/landingzone.go @@ -23,11 +23,10 @@ type MeshLandingZoneSpec struct { Description string `json:"description" tfsdk:"description"` AutomateDeletionApproval bool `json:"automateDeletionApproval" tfsdk:"automate_deletion_approval"` AutomateDeletionReplication bool `json:"automateDeletionReplication" tfsdk:"automate_deletion_replication"` - // Nullable in the API: absent means "keep the stored value". The schema defaults this to false, - // so the provider always *sends* a value — the pointer is only needed when *reading* a state - // file that an older version of this Terraform provider wrote, at a time when this field did - // not exist yet; the attribute reads back as null there. - Restricted *bool `json:"restricted,omitempty" tfsdk:"restricted"` + // Nullable in the API, where absent means "keep the stored value" — hence no `,omitempty`: the + // schema defaults this to false, so the provider always states the value it wants and never + // asks the backend to keep whatever is stored. + Restricted bool `json:"restricted" tfsdk:"restricted"` InfoLink *string `json:"infoLink,omitempty" tfsdk:"info_link"` PlatformRef UuidRef `json:"platformRef" tfsdk:"platform_ref"` PlatformProperties *MeshLandingZonePlatformProperties `json:"platformProperties,omitempty" tfsdk:"platform_properties"` From b732181e7593c1a3d9ddc5968162916e27e8fb93 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 13 Aug 2026 13:20:21 +0200 Subject: [PATCH 192/200] feat: add meshStack instance data source --- client.go | 15 ++++++------- mesh_info.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 mesh_info.go diff --git a/client.go b/client.go index d6a6372..e4b72b9 100644 --- a/client.go +++ b/client.go @@ -28,6 +28,7 @@ type Client struct { Integration MeshIntegrationClient LandingZone MeshLandingZoneClient Location MeshLocationClient + MeshInfo MeshInfoClient PaymentMethod MeshPaymentMethodClient Platform MeshPlatformClient PlatformType MeshPlatformTypeClient @@ -83,6 +84,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza Integration: newIntegrationClient(ctx, httpClient), LandingZone: newLandingZoneClient(ctx, httpClient), Location: newLocationClient(ctx, httpClient), + MeshInfo: newMeshInfoClient(httpClient), PaymentMethod: newPaymentMethodClient(ctx, httpClient), Platform: newPlatformClient(ctx, httpClient), PlatformType: newPlatformTypeClient(ctx, httpClient), @@ -106,15 +108,12 @@ func checkMeshVersion(ctx context.Context, httpClient internal.HttpClient) error return nil } - type MeshInfo struct { - Version version.Version `json:"version"` + dto, err := fetchMeshInfo(ctx, httpClient) + if err != nil { + return err } - - meshInfoEndpoint := httpClient.RootUrl.JoinPath("/mesh/info") - if meshInfo, err := internal.DoRequest[MeshInfo](ctx, httpClient, "GET", meshInfoEndpoint); err != nil { - return fmt.Errorf("failed to retrieve meshStack version information from %s endpoint: %w", meshInfoEndpoint, err) - } else if meshInfo.Version.Less(MinMeshStackVersion) { - return fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshInfo.Version, MinMeshStackVersion) + if dto.Version.Less(MinMeshStackVersion) { + return fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", dto.Version, MinMeshStackVersion) } return nil } diff --git a/mesh_info.go b/mesh_info.go new file mode 100644 index 0000000..a07bb83 --- /dev/null +++ b/mesh_info.go @@ -0,0 +1,63 @@ +package client + +import ( + "context" + "fmt" + + "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/terraform-provider-meshstack/client/version" +) + +// MeshInfo describes the meshStack instance the provider is configured against: the endpoint from +// the provider configuration, plus metadata from the public, unauthenticated /mesh/info endpoint. +type MeshInfo struct { + Endpoint string `tfsdk:"endpoint"` + Version string `tfsdk:"version"` + IsFourEyesEnabled bool `tfsdk:"is_four_eyes_enabled"` + Metadata map[string]string `tfsdk:"metadata"` + AdminWorkspaceIdentifier string `tfsdk:"admin_workspace_identifier"` +} + +// meshInfoDto is the raw /mesh/info response shape. +type meshInfoDto struct { + Version version.Version `json:"version"` + Is4EPEnabled bool `json:"is4EPEnabled"` + Metadata map[string]string `json:"metadata"` + AdminWorkspaceIdentifier string `json:"adminWorkspaceIdentifier"` +} + +type MeshInfoClient interface { + Read(ctx context.Context) (*MeshInfo, error) +} + +type meshInfoClient struct { + httpClient internal.HttpClient +} + +func newMeshInfoClient(httpClient internal.HttpClient) MeshInfoClient { + return meshInfoClient{httpClient: httpClient} +} + +func (c meshInfoClient) Read(ctx context.Context) (*MeshInfo, error) { + dto, err := fetchMeshInfo(ctx, c.httpClient) + if err != nil { + return nil, err + } + + return &MeshInfo{ + Endpoint: c.httpClient.RootUrl.String(), + Version: dto.Version.String(), + IsFourEyesEnabled: dto.Is4EPEnabled, + Metadata: dto.Metadata, + AdminWorkspaceIdentifier: dto.AdminWorkspaceIdentifier, + }, nil +} + +func fetchMeshInfo(ctx context.Context, httpClient internal.HttpClient) (meshInfoDto, error) { + meshInfoEndpoint := httpClient.RootUrl.JoinPath("/mesh/info") + dto, err := internal.DoRequest[meshInfoDto](ctx, httpClient, "GET", meshInfoEndpoint) + if err != nil { + return meshInfoDto{}, fmt.Errorf("failed to retrieve meshStack instance information from %s endpoint: %w", meshInfoEndpoint, err) + } + return dto, nil +} From dd03c54839e3830a3d22008e90e79ab37443431f Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 13 Aug 2026 11:59:36 +0200 Subject: [PATCH 193/200] refactor: address review comments regarding mesh_info struct and client --- client.go | 17 +++++++++++------ mesh_info.go | 42 ++++++++++-------------------------------- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/client.go b/client.go index e4b72b9..1016371 100644 --- a/client.go +++ b/client.go @@ -69,7 +69,8 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza }, ) - if err := checkMeshVersion(ctx, httpClient); err != nil { + meshInfoClient := newMeshInfoClient(httpClient) + if err := checkMeshVersion(ctx, meshInfoClient); err != nil { return Client{}, err } @@ -84,7 +85,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza Integration: newIntegrationClient(ctx, httpClient), LandingZone: newLandingZoneClient(ctx, httpClient), Location: newLocationClient(ctx, httpClient), - MeshInfo: newMeshInfoClient(httpClient), + MeshInfo: meshInfoClient, PaymentMethod: newPaymentMethodClient(ctx, httpClient), Platform: newPlatformClient(ctx, httpClient), PlatformType: newPlatformTypeClient(ctx, httpClient), @@ -100,7 +101,7 @@ func New(ctx context.Context, rootUrl *url.URL, userAgent string, auth Authoriza }, nil } -func checkMeshVersion(ctx context.Context, httpClient internal.HttpClient) error { +func checkMeshVersion(ctx context.Context, meshInfoClient MeshInfoClient) error { // Skip before the request, not just before the comparison: /mesh/info is a GET on the retrying // client, so an unavailable backend blocks provider configuration for the whole retry budget // (~4 minutes) and then fails it. Opting out of the check has to opt out of that too. @@ -108,12 +109,16 @@ func checkMeshVersion(ctx context.Context, httpClient internal.HttpClient) error return nil } - dto, err := fetchMeshInfo(ctx, httpClient) + info, err := meshInfoClient.Read(ctx) if err != nil { return err } - if dto.Version.Less(MinMeshStackVersion) { - return fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", dto.Version, MinMeshStackVersion) + meshVersion, err := version.Parse(info.Version) + if err != nil { + return fmt.Errorf("failed to parse meshStack version %q: %w", info.Version, err) + } + if meshVersion.Less(MinMeshStackVersion) { + return fmt.Errorf("unsupported meshStack version: meshStack is running version %s, but this client requires version %s or higher", meshVersion, MinMeshStackVersion) } return nil } diff --git a/mesh_info.go b/mesh_info.go index a07bb83..52aa911 100644 --- a/mesh_info.go +++ b/mesh_info.go @@ -5,25 +5,16 @@ import ( "fmt" "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/version" ) // MeshInfo describes the meshStack instance the provider is configured against: the endpoint from // the provider configuration, plus metadata from the public, unauthenticated /mesh/info endpoint. type MeshInfo struct { - Endpoint string `tfsdk:"endpoint"` - Version string `tfsdk:"version"` - IsFourEyesEnabled bool `tfsdk:"is_four_eyes_enabled"` - Metadata map[string]string `tfsdk:"metadata"` - AdminWorkspaceIdentifier string `tfsdk:"admin_workspace_identifier"` -} - -// meshInfoDto is the raw /mesh/info response shape. -type meshInfoDto struct { - Version version.Version `json:"version"` - Is4EPEnabled bool `json:"is4EPEnabled"` - Metadata map[string]string `json:"metadata"` - AdminWorkspaceIdentifier string `json:"adminWorkspaceIdentifier"` + Endpoint string `tfsdk:"endpoint" json:"-"` + Version string `tfsdk:"version" json:"version"` + IsFourEyesEnabled bool `tfsdk:"is_four_eyes_enabled" json:"is4EPEnabled"` + Metadata map[string]string `tfsdk:"metadata" json:"metadata"` + AdminWorkspaceIdentifier string `tfsdk:"admin_workspace_identifier" json:"adminWorkspaceIdentifier"` } type MeshInfoClient interface { @@ -39,25 +30,12 @@ func newMeshInfoClient(httpClient internal.HttpClient) MeshInfoClient { } func (c meshInfoClient) Read(ctx context.Context) (*MeshInfo, error) { - dto, err := fetchMeshInfo(ctx, c.httpClient) + meshInfoEndpoint := c.httpClient.RootUrl.JoinPath("/mesh/info") + info, err := internal.DoRequest[MeshInfo](ctx, c.httpClient, "GET", meshInfoEndpoint) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to retrieve meshStack instance information from %s endpoint: %w", meshInfoEndpoint, err) } - return &MeshInfo{ - Endpoint: c.httpClient.RootUrl.String(), - Version: dto.Version.String(), - IsFourEyesEnabled: dto.Is4EPEnabled, - Metadata: dto.Metadata, - AdminWorkspaceIdentifier: dto.AdminWorkspaceIdentifier, - }, nil -} - -func fetchMeshInfo(ctx context.Context, httpClient internal.HttpClient) (meshInfoDto, error) { - meshInfoEndpoint := httpClient.RootUrl.JoinPath("/mesh/info") - dto, err := internal.DoRequest[meshInfoDto](ctx, httpClient, "GET", meshInfoEndpoint) - if err != nil { - return meshInfoDto{}, fmt.Errorf("failed to retrieve meshStack instance information from %s endpoint: %w", meshInfoEndpoint, err) - } - return dto, nil + info.Endpoint = c.httpClient.RootUrl.String() + return &info, nil } From 7ff06961e608b29e20304d4aeede410ec9d330fb Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 13 Aug 2026 13:15:36 +0200 Subject: [PATCH 194/200] refactor: expose meshstack_instance four-eyes state as enabled_feature_flags Replaces the is_four_eyes_enabled bool with a more general enabled_feature_flags set-of-strings attribute (currently only four_eyes_role_approval), per PR review. --- mesh_info.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mesh_info.go b/mesh_info.go index 52aa911..817ceb3 100644 --- a/mesh_info.go +++ b/mesh_info.go @@ -7,12 +7,17 @@ import ( "github.com/meshcloud/terraform-provider-meshstack/client/internal" ) +// FeatureFlagFourEyesRoleApproval is the only feature flag /mesh/info can currently report in +// MeshInfo.EnabledFeatureFlags: whether the four-eyes principle (role approval) is enabled. +const FeatureFlagFourEyesRoleApproval = "four_eyes_role_approval" + // MeshInfo describes the meshStack instance the provider is configured against: the endpoint from // the provider configuration, plus metadata from the public, unauthenticated /mesh/info endpoint. type MeshInfo struct { Endpoint string `tfsdk:"endpoint" json:"-"` Version string `tfsdk:"version" json:"version"` - IsFourEyesEnabled bool `tfsdk:"is_four_eyes_enabled" json:"is4EPEnabled"` + IsFourEyesEnabled bool `tfsdk:"-" json:"is4EPEnabled"` + EnabledFeatureFlags []string `tfsdk:"enabled_feature_flags" json:"-"` Metadata map[string]string `tfsdk:"metadata" json:"metadata"` AdminWorkspaceIdentifier string `tfsdk:"admin_workspace_identifier" json:"adminWorkspaceIdentifier"` } @@ -37,5 +42,9 @@ func (c meshInfoClient) Read(ctx context.Context) (*MeshInfo, error) { } info.Endpoint = c.httpClient.RootUrl.String() + if info.IsFourEyesEnabled { + info.EnabledFeatureFlags = []string{FeatureFlagFourEyesRoleApproval} + } + return &info, nil } From 2d757f72bb0d5385a7c1ddefb346d36079801b55 Mon Sep 17 00:00:00 2001 From: Jo Schwandke Date: Thu, 13 Aug 2026 15:40:53 +0200 Subject: [PATCH 195/200] fix: put CHANGELOG entry to new version as 0.24.4 is released already. --- client_test.go | 6 +++--- mesh_info.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index 241f94e..73539cc 100644 --- a/client_test.go +++ b/client_test.go @@ -30,15 +30,15 @@ func TestCheckMeshVersion_SkipsRequestWhenOptedOut(t *testing.T) { t.Run("MESHSTACK_SKIP_VERSION_CHECK=true skips the /mesh/info request entirely", func(t *testing.T) { t.Setenv("MESHSTACK_SKIP_VERSION_CHECK", "true") httpClient, transport := newUnreachableClient() - require.NoError(t, checkMeshVersion(t.Context(), httpClient)) + require.NoError(t, checkMeshVersion(t.Context(), newMeshInfoClient(httpClient))) assert.Zero(t, transport.calls, "opting out of the version check must not send a request that can block on retries") }) t.Run("without the opt-out an unreachable /mesh/info fails", func(t *testing.T) { t.Setenv("MESHSTACK_SKIP_VERSION_CHECK", "") httpClient, transport := newUnreachableClient() - err := checkMeshVersion(t.Context(), httpClient) - require.ErrorContains(t, err, "failed to retrieve meshStack version information") + err := checkMeshVersion(t.Context(), newMeshInfoClient(httpClient)) + require.ErrorContains(t, err, "failed to retrieve meshStack instance information") assert.Equal(t, 1, transport.calls) }) } diff --git a/mesh_info.go b/mesh_info.go index 817ceb3..b70f09f 100644 --- a/mesh_info.go +++ b/mesh_info.go @@ -42,8 +42,9 @@ func (c meshInfoClient) Read(ctx context.Context) (*MeshInfo, error) { } info.Endpoint = c.httpClient.RootUrl.String() + info.EnabledFeatureFlags = []string{} if info.IsFourEyesEnabled { - info.EnabledFeatureFlags = []string{FeatureFlagFourEyesRoleApproval} + info.EnabledFeatureFlags = append(info.EnabledFeatureFlags, FeatureFlagFourEyesRoleApproval) } return &info, nil From e8a03cf3a3657ea23a963dffe75051e49df7d362 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 20 Aug 2026 05:42:29 +0200 Subject: [PATCH 196/200] refactor: point the moved client at the meshstack-cli module path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the client's own import path in the 38 files that reference it, from github.com/meshcloud/terraform-provider-meshstack/client to github.com/meshcloud/meshstack-cli/client. Nothing else changes: the package keeps the 'client' prefix it had in the provider, so this is the only edit the move needs and future 'git subtree pull' carries changes across with a conflict only where a file genuinely diverged. This is the commit that makes the subtree compile in this module. It is separate from the subtree merge on purpose — folding it in would have meant rewriting the content of every imported commit. go.sum picks up testify, which the client's tests require. Co-Authored-By: Claude Opus 5 (1M context) --- client/api_key.go | 4 ++-- client/building_block_definition.go | 6 +++--- client/building_block_definition_version.go | 6 +++--- client/building_block_definition_version_implementation.go | 4 ++-- client/building_block_definition_version_test.go | 2 +- client/building_block_run.go | 2 +- client/building_block_runner.go | 2 +- client/building_block_v2.go | 6 +++--- client/building_block_v2_test.go | 2 +- client/buildingblock.go | 2 +- client/client.go | 4 ++-- client/client_kind_test.go | 2 +- client/client_logging.go | 2 +- client/client_test.go | 2 +- client/integration.go | 2 +- client/integration_config.go | 4 ++-- client/landingzone.go | 2 +- client/location.go | 2 +- client/mesh_info.go | 2 +- client/payment_method.go | 2 +- client/platform.go | 4 ++-- client/platform_config_aks.go | 2 +- client/platform_config_aws.go | 2 +- client/platform_config_azure.go | 2 +- client/platform_config_gcp.go | 2 +- client/platform_config_kubernetes.go | 2 +- client/platform_config_openshift.go | 2 +- client/platform_type.go | 2 +- client/project.go | 2 +- client/project_group_binding.go | 2 +- client/project_user_binding.go | 2 +- client/service_instance.go | 4 ++-- client/tag_definition.go | 2 +- client/tenant_v4.go | 4 ++-- client/types/clienttypes.go | 2 +- client/workspace.go | 2 +- client/workspace_group_binding.go | 2 +- client/workspace_user_binding.go | 2 +- go.mod | 6 +++++- go.sum | 4 ++++ 40 files changed, 60 insertions(+), 52 deletions(-) diff --git a/client/api_key.go b/client/api_key.go index ec99890..f134e6e 100644 --- a/client/api_key.go +++ b/client/api_key.go @@ -3,8 +3,8 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types" ) type MeshApiKey struct { diff --git a/client/building_block_definition.go b/client/building_block_definition.go index 9ef2e7f..17d75d4 100644 --- a/client/building_block_definition.go +++ b/client/building_block_definition.go @@ -3,9 +3,9 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) type MeshBuildingBlockType string diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index 2f84dc9..3d67df6 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -6,9 +6,9 @@ import ( "errors" "fmt" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) // Enums diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index d8ae2dc..e6a9192 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -5,8 +5,8 @@ import ( "fmt" "reflect" - "github.com/meshcloud/terraform-provider-meshstack/client/types" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/types" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) type MeshBuildingBlockImplementationType string diff --git a/client/building_block_definition_version_test.go b/client/building_block_definition_version_test.go index 302b8c1..d57e237 100644 --- a/client/building_block_definition_version_test.go +++ b/client/building_block_definition_version_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/meshstack-cli/client/types" ) var ( diff --git a/client/building_block_run.go b/client/building_block_run.go index 2083188..c172bb3 100644 --- a/client/building_block_run.go +++ b/client/building_block_run.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshBuildingBlockRun struct { diff --git a/client/building_block_runner.go b/client/building_block_runner.go index 8376af3..aef560b 100644 --- a/client/building_block_runner.go +++ b/client/building_block_runner.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshBuildingBlockRunnerImplementationType string diff --git a/client/building_block_v2.go b/client/building_block_v2.go index 32171d8..8a03c44 100644 --- a/client/building_block_v2.go +++ b/client/building_block_v2.go @@ -7,9 +7,9 @@ import ( "fmt" "slices" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) type BuildingBlockLifecycleState string diff --git a/client/building_block_v2_test.go b/client/building_block_v2_test.go index 07440af..87bc246 100644 --- a/client/building_block_v2_test.go +++ b/client/building_block_v2_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) const ( diff --git a/client/buildingblock.go b/client/buildingblock.go index c8db231..a2e2f18 100644 --- a/client/buildingblock.go +++ b/client/buildingblock.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) const ( diff --git a/client/client.go b/client/client.go index 1016371..a9e293e 100644 --- a/client/client.go +++ b/client/client.go @@ -7,8 +7,8 @@ import ( "os" "time" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/version" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/version" ) var MinMeshStackVersion = version.MustParse("2026.34.0") diff --git a/client/client_kind_test.go b/client/client_kind_test.go index 31a6b20..7d19572 100644 --- a/client/client_kind_test.go +++ b/client/client_kind_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) func TestKind(t *testing.T) { diff --git a/client/client_logging.go b/client/client_logging.go index ed6979a..e5811b8 100644 --- a/client/client_logging.go +++ b/client/client_logging.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/internal" +import "github.com/meshcloud/meshstack-cli/client/internal" // Logger exposes logging for client operations within this package (including internal). type Logger = internal.Logger diff --git a/client/client_test.go b/client/client_test.go index 73539cc..d0b9c65 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type erroringRoundTripper struct{ calls int } diff --git a/client/integration.go b/client/integration.go index 1753647..b46055c 100644 --- a/client/integration.go +++ b/client/integration.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshIntegration struct { diff --git a/client/integration_config.go b/client/integration_config.go index 5e23cda..c4a92fc 100644 --- a/client/integration_config.go +++ b/client/integration_config.go @@ -5,8 +5,8 @@ import ( "fmt" "reflect" - "github.com/meshcloud/terraform-provider-meshstack/client/types" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/types" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) type MeshIntegrationConfigType string diff --git a/client/landingzone.go b/client/landingzone.go index 346c48f..5af9273 100644 --- a/client/landingzone.go +++ b/client/landingzone.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshLandingZone struct { diff --git a/client/location.go b/client/location.go index d3e3e0a..c2935ca 100644 --- a/client/location.go +++ b/client/location.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshLocation struct { diff --git a/client/mesh_info.go b/client/mesh_info.go index b70f09f..682463d 100644 --- a/client/mesh_info.go +++ b/client/mesh_info.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) // FeatureFlagFourEyesRoleApproval is the only feature flag /mesh/info can currently report in diff --git a/client/payment_method.go b/client/payment_method.go index f32ffc1..443c10a 100644 --- a/client/payment_method.go +++ b/client/payment_method.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshPaymentMethod struct { diff --git a/client/platform.go b/client/platform.go index 9c0bbad..cc303c5 100644 --- a/client/platform.go +++ b/client/platform.go @@ -3,8 +3,8 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types" ) type MeshPlatform struct { diff --git a/client/platform_config_aks.go b/client/platform_config_aks.go index 8521a30..56d0722 100644 --- a/client/platform_config_aks.go +++ b/client/platform_config_aks.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/types" +import "github.com/meshcloud/meshstack-cli/client/types" type AksPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` diff --git a/client/platform_config_aws.go b/client/platform_config_aws.go index 55fa205..dfd64e1 100644 --- a/client/platform_config_aws.go +++ b/client/platform_config_aws.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/types" +import "github.com/meshcloud/meshstack-cli/client/types" type AwsPlatformConfig struct { Region string `json:"region,omitempty" tfsdk:"region"` diff --git a/client/platform_config_azure.go b/client/platform_config_azure.go index b5d68aa..1c60058 100644 --- a/client/platform_config_azure.go +++ b/client/platform_config_azure.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/types" +import "github.com/meshcloud/meshstack-cli/client/types" type AzurePlatformConfig struct { EntraTenant string `json:"entraTenant" tfsdk:"entra_tenant"` diff --git a/client/platform_config_gcp.go b/client/platform_config_gcp.go index 3c27767..fa6f709 100644 --- a/client/platform_config_gcp.go +++ b/client/platform_config_gcp.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/types" +import "github.com/meshcloud/meshstack-cli/client/types" type GcpPlatformConfig struct { Replication *GcpReplicationConfig `json:"replication,omitempty" tfsdk:"replication"` diff --git a/client/platform_config_kubernetes.go b/client/platform_config_kubernetes.go index e4b34cf..cc7ad9a 100644 --- a/client/platform_config_kubernetes.go +++ b/client/platform_config_kubernetes.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/types" +import "github.com/meshcloud/meshstack-cli/client/types" type KubernetesPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` diff --git a/client/platform_config_openshift.go b/client/platform_config_openshift.go index 123c6d8..565c1b0 100644 --- a/client/platform_config_openshift.go +++ b/client/platform_config_openshift.go @@ -1,6 +1,6 @@ package client -import "github.com/meshcloud/terraform-provider-meshstack/client/types" +import "github.com/meshcloud/meshstack-cli/client/types" type OpenShiftPlatformConfig struct { BaseUrl string `json:"baseUrl" tfsdk:"base_url"` diff --git a/client/platform_type.go b/client/platform_type.go index 270a5c6..c9cc33c 100644 --- a/client/platform_type.go +++ b/client/platform_type.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshPlatformType struct { diff --git a/client/project.go b/client/project.go index 1f9078b..434688f 100644 --- a/client/project.go +++ b/client/project.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshProject struct { diff --git a/client/project_group_binding.go b/client/project_group_binding.go index 90eda95..85872ef 100644 --- a/client/project_group_binding.go +++ b/client/project_group_binding.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshProjectGroupBinding struct { diff --git a/client/project_user_binding.go b/client/project_user_binding.go index d6ed6ca..2b5b418 100644 --- a/client/project_user_binding.go +++ b/client/project_user_binding.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshProjectUserBinding struct { diff --git a/client/service_instance.go b/client/service_instance.go index 74918e7..0113b44 100644 --- a/client/service_instance.go +++ b/client/service_instance.go @@ -3,8 +3,8 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types" ) type MeshServiceInstance struct { diff --git a/client/tag_definition.go b/client/tag_definition.go index 2844d0c..f298a39 100644 --- a/client/tag_definition.go +++ b/client/tag_definition.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) const API_VERSION_TAG_DEFINITION = "v1" diff --git a/client/tenant_v4.go b/client/tenant_v4.go index 195f270..3aa48a9 100644 --- a/client/tenant_v4.go +++ b/client/tenant_v4.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" - "github.com/meshcloud/terraform-provider-meshstack/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/internal" + "github.com/meshcloud/meshstack-cli/client/types/enum" ) type TenantLifecycleState string diff --git a/client/types/clienttypes.go b/client/types/clienttypes.go index e0ec320..f2a1737 100644 --- a/client/types/clienttypes.go +++ b/client/types/clienttypes.go @@ -4,7 +4,7 @@ import ( "reflect" "strings" - "github.com/meshcloud/terraform-provider-meshstack/client/types/variant" + "github.com/meshcloud/meshstack-cli/client/types/variant" ) type ( diff --git a/client/workspace.go b/client/workspace.go index 1950b50..36f9b17 100644 --- a/client/workspace.go +++ b/client/workspace.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshWorkspace struct { diff --git a/client/workspace_group_binding.go b/client/workspace_group_binding.go index 1ff9739..cc56cd3 100644 --- a/client/workspace_group_binding.go +++ b/client/workspace_group_binding.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshWorkspaceGroupBinding struct { diff --git a/client/workspace_user_binding.go b/client/workspace_user_binding.go index 13d3ebb..1b9cdd2 100644 --- a/client/workspace_user_binding.go +++ b/client/workspace_user_binding.go @@ -3,7 +3,7 @@ package client import ( "context" - "github.com/meshcloud/terraform-provider-meshstack/client/internal" + "github.com/meshcloud/meshstack-cli/client/internal" ) type MeshWorkspaceUserBinding struct { diff --git a/go.mod b/go.mod index a149f44..cbebf6a 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,13 @@ module github.com/meshcloud/meshstack-cli go 1.26 // keep flake.nix's pinned Go (go_1_26 + GOROOT) in lock-step when bumping -require github.com/spf13/cobra v1.10.2 +require ( + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.12.1 +) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.9 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect ) diff --git a/go.sum b/go.sum index a6ee3e0..3f5d306 100644 --- a/go.sum +++ b/go.sum @@ -6,5 +6,9 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 232059250500d401c6a9bbb510213b4e486755bf Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 20 Aug 2026 05:43:05 +0200 Subject: [PATCH 197/200] feat: add pkg/login, the shared credential resolution pkg/login is the entry point both the CLI and the Terraform provider use to turn credentials into a client.Authorization. It resolves credentials and nothing more. The login exchange stays in client/internal/auth.go, where it already caches the access token and refreshes it before expiry; Go's internal rule keeps that code inside client/, and it is reached through client.NewApiKeyAuthorization. Writing a second exchange here would have produced a static token that starts returning 401 once it expires. The four MESHSTACK_ environment variable names are exported consts rather than private ones, because the provider's diagnostics quote the variable names in their message text; keeping them private would mean the same strings living in both repositories. Credentials.Merge exists for the provider: it merges provider block attributes over FromEnv so an explicitly configured attribute outranks the environment. One gap worth recording. Authorization.Header takes a client/internal.HttpClient, so no package outside client/ can call it, and nothing can read the cached token back out either. That does not matter yet, but caching a token in ~/.config/meshstack-cli will need a new exported seam. pkg/login is the only place that constructs an Authorization, which is where that seam belongs. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/login/apikey.go | 106 ++++++++++++++++++ pkg/login/apikey_test.go | 231 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 pkg/login/apikey.go create mode 100644 pkg/login/apikey_test.go diff --git a/pkg/login/apikey.go b/pkg/login/apikey.go new file mode 100644 index 0000000..1487093 --- /dev/null +++ b/pkg/login/apikey.go @@ -0,0 +1,106 @@ +// Package login resolves meshStack API credentials and turns them into a +// client.Authorization. +// +// It is shared by the meshStack CLI and the meshStack Terraform provider, so both +// read the same environment variables and run the same login exchange. The +// exchange itself is not here: it lives in client/internal, which Go's internal +// rule keeps inside client/, and is reached through client.NewApiKeyAuthorization. +// A second, hand-rolled exchange would get a static token and start returning 401 +// once it expired. +package login + +import ( + "fmt" + "net/url" + "os" + + "github.com/meshcloud/meshstack-cli/client" +) + +// Environment variables holding meshStack API credentials. They are exported so +// that callers can name them in their own error messages — the Terraform provider +// does, because its diagnostics have to mention both the provider attribute and +// the variable — and so that both repositories share one definition. +const ( + EnvKeyEndpoint = "MESHSTACK_ENDPOINT" + EnvKeyApiKey = "MESHSTACK_API_KEY" + EnvKeyApiSecret = "MESHSTACK_API_SECRET" + EnvKeyApiToken = "MESHSTACK_API_TOKEN" +) + +// Credentials addresses one meshStack API. The fields are unvalidated: an empty +// field means "not configured", which lets a caller merge several sources before +// deciding whether anything is missing. +type Credentials struct { + Endpoint string + // ApiKey and ApiSecret are exchanged for an access token at every client + // creation. This is the pair to prefer, because the client refreshes the + // token it receives. + ApiKey string + ApiSecret string + // ApiToken is an access token that is already valid, and skips the exchange. + // It takes precedence over ApiKey and ApiSecret. Nothing can refresh it, so it + // expires during long-running work. + ApiToken string +} + +// FromEnv reads credentials from the environment. Variables that are unset yield +// empty fields rather than an error, so a caller with another source of +// configuration can fill them in. +func FromEnv() Credentials { + return Credentials{ + Endpoint: os.Getenv(EnvKeyEndpoint), + ApiKey: os.Getenv(EnvKeyApiKey), + ApiSecret: os.Getenv(EnvKeyApiSecret), + ApiToken: os.Getenv(EnvKeyApiToken), + } +} + +// Merge returns c with every non-empty field of override applied on top. Callers +// that read credentials from more than one place use this to rank the sources: +// the Terraform provider merges its provider block attributes over FromEnv, so an +// explicitly configured attribute wins over the environment. +func (c Credentials) Merge(override Credentials) Credentials { + if override.Endpoint != "" { + c.Endpoint = override.Endpoint + } + if override.ApiKey != "" { + c.ApiKey = override.ApiKey + } + if override.ApiSecret != "" { + c.ApiSecret = override.ApiSecret + } + if override.ApiToken != "" { + c.ApiToken = override.ApiToken + } + return c +} + +// EndpointURL parses the endpoint into the form client.New expects. +func (c Credentials) EndpointURL() (*url.URL, error) { + if c.Endpoint == "" { + return nil, fmt.Errorf("meshStack endpoint is not configured, set the %s environment variable", EnvKeyEndpoint) + } + endpoint, err := url.Parse(c.Endpoint) + if err != nil { + return nil, fmt.Errorf("meshStack endpoint %q is not a valid URL: %w", c.Endpoint, err) + } + return endpoint, nil +} + +// Authorization builds the authorization to hand to client.New. It reports what is +// missing rather than producing an authorization that fails on first use. +func (c Credentials) Authorization() (client.Authorization, error) { + if c.ApiToken != "" { + return client.NewApiTokenAuthorization(c.ApiToken), nil + } + switch { + case c.ApiKey == "" && c.ApiSecret == "": + return nil, fmt.Errorf("meshStack API credentials are not configured, set the %s and %s environment variables", EnvKeyApiKey, EnvKeyApiSecret) + case c.ApiKey == "": + return nil, fmt.Errorf("meshStack API key is not configured, set the %s environment variable", EnvKeyApiKey) + case c.ApiSecret == "": + return nil, fmt.Errorf("meshStack API secret is not configured, set the %s environment variable", EnvKeyApiSecret) + } + return client.NewApiKeyAuthorization(c.ApiKey, c.ApiSecret), nil +} diff --git a/pkg/login/apikey_test.go b/pkg/login/apikey_test.go new file mode 100644 index 0000000..44ccff6 --- /dev/null +++ b/pkg/login/apikey_test.go @@ -0,0 +1,231 @@ +package login_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/meshstack-cli/pkg/login" +) + +func TestFromEnv(t *testing.T) { + for name, testCase := range map[string]struct { + environment map[string]string + want login.Credentials + }{ + "all four variables set": { + environment: map[string]string{ + login.EnvKeyEndpoint: "https://api.my.meshstack.io", + login.EnvKeyApiKey: "key", + login.EnvKeyApiSecret: "secret", + login.EnvKeyApiToken: "token", + }, + want: login.Credentials{ + Endpoint: "https://api.my.meshstack.io", + ApiKey: "key", + ApiSecret: "secret", + ApiToken: "token", + }, + }, + "key and secret without a token": { + environment: map[string]string{ + login.EnvKeyEndpoint: "https://api.my.meshstack.io", + login.EnvKeyApiKey: "key", + login.EnvKeyApiSecret: "secret", + }, + want: login.Credentials{ + Endpoint: "https://api.my.meshstack.io", + ApiKey: "key", + ApiSecret: "secret", + }, + }, + "an empty variable reads as unset": { + environment: map[string]string{login.EnvKeyEndpoint: ""}, + want: login.Credentials{}, + }, + "nothing set": { + environment: map[string]string{}, + want: login.Credentials{}, + }, + } { + t.Run(name, func(t *testing.T) { + // Clear all four first, so a variable left over from the developer's own + // shell cannot make a case pass. + for _, key := range []string{login.EnvKeyEndpoint, login.EnvKeyApiKey, login.EnvKeyApiSecret, login.EnvKeyApiToken} { + t.Setenv(key, "") + } + for key, value := range testCase.environment { + t.Setenv(key, value) + } + + assert.Equal(t, testCase.want, login.FromEnv()) + }) + } +} + +func TestMerge(t *testing.T) { + environment := login.Credentials{ + Endpoint: "https://from-env.meshstack.io", + ApiKey: "env-key", + ApiSecret: "env-secret", + ApiToken: "env-token", + } + + for name, testCase := range map[string]struct { + override login.Credentials + want login.Credentials + }{ + "an empty override changes nothing": { + override: login.Credentials{}, + want: environment, + }, + "a non-empty field wins": { + override: login.Credentials{Endpoint: "https://explicit.meshstack.io"}, + want: login.Credentials{ + Endpoint: "https://explicit.meshstack.io", + ApiKey: "env-key", + ApiSecret: "env-secret", + ApiToken: "env-token", + }, + }, + "empty fields of the override do not clear the receiver": { + override: login.Credentials{ApiKey: "explicit-key"}, + want: login.Credentials{ + Endpoint: "https://from-env.meshstack.io", + ApiKey: "explicit-key", + ApiSecret: "env-secret", + ApiToken: "env-token", + }, + }, + "every field overridden at once": { + override: login.Credentials{ + Endpoint: "https://explicit.meshstack.io", + ApiKey: "explicit-key", + ApiSecret: "explicit-secret", + ApiToken: "explicit-token", + }, + want: login.Credentials{ + Endpoint: "https://explicit.meshstack.io", + ApiKey: "explicit-key", + ApiSecret: "explicit-secret", + ApiToken: "explicit-token", + }, + }, + } { + t.Run(name, func(t *testing.T) { + assert.Equal(t, testCase.want, environment.Merge(testCase.override)) + }) + } +} + +func TestEndpointURL(t *testing.T) { + for name, testCase := range map[string]struct { + endpoint string + want string + wantErr string + }{ + "a valid URL": { + endpoint: "https://api.my.meshstack.io", + want: "https://api.my.meshstack.io", + }, + "a URL with a port and path": { + endpoint: "http://localhost:8080/api", + want: "http://localhost:8080/api", + }, + "an empty endpoint names its variable": { + endpoint: "", + wantErr: login.EnvKeyEndpoint, + }, + "an unparseable endpoint is reported": { + endpoint: "://no-scheme", + wantErr: "not a valid URL", + }, + } { + t.Run(name, func(t *testing.T) { + endpoint, err := login.Credentials{Endpoint: testCase.endpoint}.EndpointURL() + + if testCase.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), testCase.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, testCase.want, endpoint.String()) + }) + } +} + +func TestAuthorization(t *testing.T) { + // Header cannot be called from outside client/: it takes a + // client/internal.HttpClient, which no other package can name. So an + // authorization is identified by comparing it against one built from a single + // credential source. + fromToken, err := login.Credentials{ApiToken: "token"}.Authorization() + require.NoError(t, err) + fromKeySecret, err := login.Credentials{ApiKey: "key", ApiSecret: "secret"}.Authorization() + require.NoError(t, err) + + t.Run("the two credential sources build different authorizations", func(t *testing.T) { + // Guards the comparisons below: if both sources produced the same value, the + // precedence cases would pass for the wrong reason. + assert.NotEqual(t, fromToken, fromKeySecret) + }) + + for name, testCase := range map[string]struct { + credentials login.Credentials + want login.Credentials + }{ + "a token alone": { + credentials: login.Credentials{ApiToken: "token"}, + want: login.Credentials{ApiToken: "token"}, + }, + "a token outranks a key and secret": { + credentials: login.Credentials{ApiKey: "key", ApiSecret: "secret", ApiToken: "token"}, + want: login.Credentials{ApiToken: "token"}, + }, + "a key and secret alone": { + credentials: login.Credentials{ApiKey: "key", ApiSecret: "secret"}, + want: login.Credentials{ApiKey: "key", ApiSecret: "secret"}, + }, + } { + t.Run(name, func(t *testing.T) { + want, err := testCase.want.Authorization() + require.NoError(t, err) + + got, err := testCase.credentials.Authorization() + + require.NoError(t, err) + assert.Equal(t, want, got) + }) + } +} + +func TestAuthorizationNamesTheMissingVariable(t *testing.T) { + for name, testCase := range map[string]struct { + credentials login.Credentials + wants []string + }{ + "nothing configured": { + credentials: login.Credentials{}, + wants: []string{login.EnvKeyApiKey, login.EnvKeyApiSecret}, + }, + "a secret without a key": { + credentials: login.Credentials{ApiSecret: "secret"}, + wants: []string{login.EnvKeyApiKey}, + }, + "a key without a secret": { + credentials: login.Credentials{ApiKey: "key"}, + wants: []string{login.EnvKeyApiSecret}, + }, + } { + t.Run(name, func(t *testing.T) { + _, err := testCase.credentials.Authorization() + + require.Error(t, err) + for _, want := range testCase.wants { + assert.Contains(t, err.Error(), want) + } + }) + } +} From 0454c95ce2572f79a45f0372683c146229e8a92b Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Tue, 18 Aug 2026 15:49:46 +0200 Subject: [PATCH 198/200] feat: release with goreleaser and publish a container image Adds the release path now, while the repository is still small, rather than discovering its rough edges at the first tag. Pushing a v* tag runs goreleaser, which publishes archives and checksums for linux, darwin and windows on amd64 and arm64, and then builds the container image for the same tag. The image build is a separate job so a failing image does not take the archives down with it, and it is a reusable workflow so a push to main can refresh :main through the same code path. Without that, no usable image would exist until the first release. Naming follows what the repository publishes rather than what it builds: the archives, the checksum file and the image are all meshstack-cli, while the binary inside them is meshstack. goreleaser's project_name carries the former and builds[].binary the latter, and the image entrypoint is the meshstack binary, so 'docker run ghcr.io/meshcloud/meshstack-cli buildingblock list' reads like the local invocation. The version reaches the binary as an ldflag on main.Version in cmd/meshstack, which the goreleaser config and the Dockerfile have to keep in agreement. A build without it reports 'dev'. Images go to GHCR only. Pull requests build the image without pushing it, so a broken Dockerfile fails review instead of main. The Dockerfile cross-compiles from the build platform using buildx's TARGETOS/TARGETARCH rather than emulating the target, and ships the binary on distroless static, which comes to 4.6 MB. Tags are computed in a shell step instead of with docker/metadata-action, to keep the set of SHA-pinned actions small. The action SHAs come from meshcloud/building-block-runner, whose image workflow this follows. Verified locally: 'goreleaser check' passes, 'goreleaser release --snapshot' produces meshstack-cli_*.tar.gz archives containing a meshstack binary that reports the injected version, and the image builds and runs. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 11 ++++ .github/workflows/build-image.yml | 84 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 46 +++++++++++++++++ .gitignore | 3 ++ .goreleaser.yml | 67 ++++++++++++++++++++++++ AGENTS.md | 32 ++++++++++-- Dockerfile | 30 +++++++++++ README.md | 9 ++-- Taskfile.yml | 16 ++++++ flake.nix | 3 ++ 10 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-image.yml create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b180329 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# The build only needs the Go sources, go.mod and go.sum. Everything below would +# otherwise be copied into the build context and invalidate its cache. +.git/ +.github/ +dist/ +.nix-go/ +meshstack +.env +.vscode/ +.idea/ +*.md diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..7bcddd7 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,84 @@ +# Builds the meshstack container image and pushes it to GHCR only. Modelled on +# meshcloud/building-block-runner's build-images.yml, minus the Docker Hub push. +name: Build Image + +env: + REGISTRY: ghcr.io + IMAGE_NAMESPACE: ${{ github.repository_owner }} + IMAGE_NAME: meshstack-cli + +on: + # Called by the release workflow, so a tagged release publishes the matching image. + workflow_call: + inputs: + version: + description: "Release version to tag the image with, e.g. v1.2.3" + required: true + type: string + # A push to main refreshes :main, which is what makes the image usable before the + # first release exists. + push: + branches: + - main + # Pull requests build the image but do not push it, so a broken Dockerfile fails + # review rather than main. + pull_request: + paths: + - 'Dockerfile' + - '.github/workflows/build-image.yml' + - 'go.mod' + - 'go.sum' + - '**/*.go' + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + # Tags are computed here rather than with docker/metadata-action, to keep the + # set of pinned actions small. + - name: Determine version and tags + id: meta + run: | + if [ -n "${{ inputs.version }}" ]; then + version="${{ inputs.version }}" + tags="${REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:${version}" + tags="${tags},${REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:latest" + elif [ "${{ github.ref }}" = "refs/heads/main" ]; then + version="main-$(git rev-parse --short HEAD)" + tags="${REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:main" + tags="${tags},${REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:${version}" + else + version="pr-${{ github.event.number }}" + tags="${REGISTRY}/${IMAGE_NAMESPACE}/${IMAGE_NAME}:${version}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "tags=${tags}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + VERSION=${{ steps.meta.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0a71d8b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +# Releases the meshstack CLI when a tag matching "v*" is pushed. +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + goreleaser: + name: GoReleaser + runs-on: ubuntu-latest + permissions: + # Creating a release and uploading its assets counts as writing contents. + contents: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # Let goreleaser read older tags, which it needs for the changelog. + fetch-depth: 0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: 'go.mod' + cache: true + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + args: release --clean + env: + # GitHub sets GITHUB_TOKEN automatically. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Publishes the image for the same tag. Separate job so a failing image build does + # not take the archives down with it. + image: + name: Image + needs: [ goreleaser ] + permissions: + contents: read + packages: write + uses: ./.github/workflows/build-image.yml + with: + version: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index 20d33a3..d8ce84b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Binary produced by 'task build' /meshstack +# Release artifacts produced by goreleaser +/dist/ + # Go environment created by the Nix dev shell (flake.nix shellHook) /.nix-go/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..847cd96 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,67 @@ +# Visit https://goreleaser.com for documentation on how to customize this behavior. +version: 2 + +# Everything published carries the repository name, meshstack-cli: the archives, the +# checksum file and the container image. The binary inside them is meshstack, which +# is why the build below names it explicitly instead of inheriting project_name. +project_name: meshstack-cli + +before: + hooks: + - go mod tidy + +builds: + - main: ./cmd/meshstack + binary: meshstack + env: + # A statically linked binary runs in the distroless container image and on any + # glibc version. + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w -X main.Version={{ .Version }}' + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + # There is no 32-bit x86 or arm64 Windows target worth publishing. + ignore: + - goos: windows + goarch: arm64 + +archives: + - formats: + - tar.gz + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' + format_overrides: + - goos: windows + formats: + - zip + +checksum: + name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' + algorithm: sha256 + +changelog: + # Conventional Commits, so group the notes by type and drop the noise. + use: github + sort: asc + groups: + - title: Features + regexp: '^feat(\(.+\))?!?:' + order: 0 + - title: Fixes + regexp: '^fix(\(.+\))?!?:' + order: 1 + - title: Others + order: 99 + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' diff --git a/AGENTS.md b/AGENTS.md index 4e52fa1..557e766 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,9 @@ a library. This file is the always-on source of truth for both AI agents and hum - **meshStack CLI** — the product name, used in prose and docs. - `github.com/meshcloud/meshstack-cli` — the repository and Go module. +Everything *published* carries the repository name — the release archives, the checksum file and the +container image are all `meshstack-cli` — while the binary inside them is `meshstack`. + The binary gets its name from its directory, `cmd/meshstack`, which is why there is no `-o` flag anywhere: `go build ./cmd/meshstack` and `go install github.com/meshcloud/meshstack-cli/cmd/meshstack@latest` both produce `meshstack`. **Do @@ -91,10 +94,13 @@ on disk will hook in later. Everything runs through the Taskfile, inside `nix develop`: ```shell -task build # ./meshstack -task test # go test ./... -task lint # golangci-lint run -task tidy # go mod tidy +task build # ./meshstack +task test # go test ./... +task lint # golangci-lint run +task tidy # go mod tidy +task release:check # validate .goreleaser.yml +task release:snapshot # build the release artifacts into dist/ without publishing +task image # docker build -t meshstack:dev ``` The Go version is pinned in `go.mod` and in `flake.nix`; **keep them in lock-step when bumping**, and @@ -108,3 +114,21 @@ provider and the CLI share one definition — use those consts rather than the s Taskfile reads a git-ignored `.env` for local runs. `MESHSTACK_SKIP_VERSION_CHECK=true` skips the minimum backend version check in `client/client.go`. + +## Releasing + +Pushing a `v*` tag runs goreleaser, which publishes the archives and checksums, and then builds the +container image for the same tag. The image goes to GHCR only, as +`ghcr.io/meshcloud/meshstack-cli`, and its entrypoint is the `meshstack` binary, so +`docker run ghcr.io/meshcloud/meshstack-cli buildingblock list` reads like the local invocation. A +push to `main` refreshes `:main`, so an image exists before the first release does. + + +The version comes from the git tag through an ldflag on `main.Version` in `cmd/meshstack`, in two +places that must agree: `.goreleaser.yml` and the `Dockerfile`. A build without the ldflag reports +`dev`, which is correct for a working copy but must never reach a published artifact — check with +`meshstack --version` after `task release:snapshot`. + + +Pin every GitHub Action by commit SHA with the version in a trailing comment, as the existing +workflows do. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aac96bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Runs on the build platform and cross-compiles for TARGETOS/TARGETARCH, so a +# multi-platform build needs no emulation. buildx sets those two args itself. +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build + +WORKDIR /src + +# Copied on their own so the module download layer survives any source change. +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +ARG TARGETOS +ARG TARGETARCH +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ + -trimpath \ + -ldflags "-s -w -X main.Version=${VERSION}" \ + -o /out/meshstack ./cmd/meshstack + +# distroless static: no shell and no package manager, which is all a single static +# binary needs. 'nonroot' runs as uid 65532. +FROM gcr.io/distroless/static-debian12:nonroot + +# The image is named after the repository, meshstack-cli, while the binary it carries +# is meshstack. So `docker run ghcr.io/meshcloud/meshstack-cli buildingblock list` +# reads the same as the local `meshstack buildingblock list`. +COPY --from=build /out/meshstack /usr/local/bin/meshstack + +ENTRYPOINT ["/usr/local/bin/meshstack"] diff --git a/README.md b/README.md index 845c1ca..e9b6d47 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,14 @@ go install github.com/meshcloud/meshstack-cli/cmd/meshstack@latest ## Development -The Nix dev shell provides Go, `golangci-lint` and `task`: +The Nix dev shell provides Go, `golangci-lint`, `goreleaser` and `task`: ```shell nix develop -task build # ./meshstack -task test # go test ./... -task lint # golangci-lint run, add -- --fix to apply fixes +task build # ./meshstack +task test # go test ./... +task lint # golangci-lint run, add -- --fix to apply fixes +task release:snapshot # build the release artifacts without publishing ``` The Go version is pinned in `go.mod` and in `flake.nix`, and is kept in lock-step with the diff --git a/Taskfile.yml b/Taskfile.yml index 0b377e1..5d89c7c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -28,7 +28,23 @@ tasks: cmds: - go mod tidy + release:check: + desc: Validate .goreleaser.yml + cmds: + - goreleaser check + + release:snapshot: + desc: Build the release artifacts into dist/ without publishing them + cmds: + - goreleaser release --snapshot --clean {{.CLI_ARGS}} + + image: + desc: Build the container image locally + cmds: + - docker build -t meshstack:dev {{.CLI_ARGS}} . + clean: desc: Remove build artifacts cmds: - rm -f meshstack + - rm -rf dist diff --git a/flake.nix b/flake.nix index 2d71b36..f9c1f21 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,9 @@ # https://taskfile.dev go-task + + # https://goreleaser.com — task release:check / release:snapshot + goreleaser ]; shellHook = '' From 163b9769b85aad6a510fda0bd2d7e786cfe70494 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 20 Aug 2026 05:44:42 +0200 Subject: [PATCH 199/200] docs: record that client/ is a git subtree and how to sync it The client came in with 'git subtree add' rather than as a copy, which changes two things a contributor needs to know and cannot guess from the tree. Changes travel with 'git subtree pull' and 'git subtree push' against the Terraform provider. A pull conflicts only where a file genuinely diverged, since the one local edit the move needed was rewriting the client's own import path. Reading the pre-import history takes both paths. The split history carries the files at the repository root and the import merge re-roots them under client/, so 'git log -- client/client.go' stops at the merge while 'git log -- client/client.go client.go' shows all of it. 'git blame' traverses the merge on its own, and 'git log --follow' does not help, because it resolves renames within a commit's parents rather than across a subtree re-rooting. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 557e766..a0a2f60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,8 +59,26 @@ checksum database. `depguard` in `.golangci.yml` enforces the boundaries per dir rules as the policy. Widening them is a deliberate decision, not a lint fix. -`client/` keeps the import path it had in the Terraform provider, so moving code between the two -repositories stays a plain path rewrite. +`client/` is a **git subtree** imported from +[terraform-provider-meshstack](https://github.com/meshcloud/terraform-provider-meshstack), where it +used to live, and it keeps the `client` path prefix it had there. Carry changes across with +`git subtree`, not by copying files: + +```shell +git subtree pull --prefix=client https://github.com/meshcloud/terraform-provider-meshstack.git main +git subtree push --prefix=client https://github.com/meshcloud/terraform-provider-meshstack.git +``` + +A pull conflicts only where a file genuinely diverged, because the one local edit the move needed was +rewriting the client's own import path. + +Reading the pre-import history takes both paths, since the split history carries the files at the +repository root and the import merge re-roots them under `client/`: + +```shell +git log -- client/client.go client.go # a path-limited log from client/ alone stops at the merge +git blame client/client.go # traverses the merge on its own +``` The login exchange lives in `client/internal/auth.go`, which posts to `/api/login`, caches the access token and refreshes it before expiry. Go's internal rule keeps that code inside `client/`; reach it From 0b774435e0f6e0c5eb7d7edff586eaf9a50b0067 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Thu, 20 Aug 2026 05:51:18 +0200 Subject: [PATCH 200/200] fix: make CI's lint formatting deterministic, and stop filtering to changed code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lint job failed on this branch with a gofmt-class finding on client/internal/retry_test.go:46 that no local run reproduces. The cause is which Go built the linter, not which Go is on PATH. golangci-lint's formatters use the go/format compiled into the binary, so 'version: latest' with the default binary install downloaded a 2.13.0 built with Go 1.27, while go.mod pins 1.26. Go 1.27 widens end-of-line comment alignment groups, so it wants that table's comments aligned to a far wider column — and Go 1.26 then rejects the result. The two are mutually exclusive: no formatting of that file satisfies both, which rules out simply reformatting it. So the linter is now built here from source with go.mod's Go ('install-mode: goinstall') and pinned to v2.13.0. Formatting is then decided by the Go the code is written against, and a linter upgrade becomes a deliberate edit. setup-go takes its version from go.mod for the same reason: it is what builds the linter. Verified by controlled comparison, because two earlier explanations were wrong. Same config, same file, pristine checkout, empty cache: golangci-lint 2.13.0 built with Go 1.26.5 reports a clean tree, the official 2.13.0 binary built with Go 1.27.0 reports the finding, and each rejects the other's preferred formatting. Neither the patch-filtering mode nor the toolchain on PATH changes that. Dropping only-new-issues is the second half. It exists so a pull request against a large legacy codebase is not buried in pre-existing findings; this repository starts clean and CI keeps it clean from the first commit, so filtering to changed code cannot help and can only hide a real finding. It hid this one, and the provider's identical job still hides it. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 19 +++++++++++++++---- AGENTS.md | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9659ee..97c73d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,17 +43,28 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: read # Required for only-new-issues on PRs steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: stable + # The repository's pinned Go, because it is what builds the linter below. + go-version-file: 'go.mod' - name: golangci-lint uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: latest - only-new-issues: true # Show only issues in changed code on PRs + # golangci-lint's formatters use the go/format compiled into the binary, so the + # formatting they enforce comes from the Go release that BUILT the linter, not + # from the toolchain on PATH. The published binaries are built with whatever Go + # was current at release time, which made 'version: latest' with install-mode + # 'binary' enforce a different gofmt than the pinned Go: 1.27 widens end-of-line + # comment alignment groups, and 1.26 rejects the result, so the two disagree with + # no formatting that satisfies both. Building the linter here with go.mod's Go + # ties formatting to the version the code is written against. + install-mode: goinstall + version: v2.13.0 + # Deliberately no only-new-issues: this repository starts clean and CI keeps it + # that way, so filtering to changed code cannot help and can only hide a finding. + # It hid this one, and still hides it in the provider's identical job. - name: Suggest fix command on failure if: failure() run: | diff --git a/AGENTS.md b/AGENTS.md index a0a2f60..023b84f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,6 +99,11 @@ on disk will hook in later. prefer one sharp line over a paragraph. - **Lint only via `task lint`** (golangci-lint, which also enforces gci import ordering and gofmt). It already runs `govet`, so **do not run `go vet` separately**. Auto-fix with `task lint -- --fix`. + CI builds golangci-lint from source with `go.mod`'s Go (`install-mode: goinstall`) instead of + downloading a release binary, and that is not incidental. The formatters use the `go/format` + compiled into the linter, so a binary built with a newer Go enforces a different gofmt than the one + the code is written against — and the two can disagree with no formatting that satisfies both. + Switching CI to the faster binary install brings that back. - **Conventional Commits** for messages (`feat:`, `fix:`, `docs:`, `chore:`, `feat!:` for breaking). - **Stress-test a plan before writing code.** For any non-trivial change, walk each branch of the decision tree and settle every open question with a recommended answer first. Catching a wrong turn