Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,27 @@ nohup ./gradlew :buildingblocks:block-coordinator-api:start --console=plain > /t
nohup ./gradlew :buildingblocks:manual-block-runner:start --console=plain > /tmp/manual-runner.log 2>&1 &
nohup ./gradlew :meshfed:replicator:replicator-api:start --console=plain > /tmp/replicator.log 2>&1 &

# 3. Wait for meshfed-api to be ready (takes ~60-120s for first start)
# 3. Wait for ALL services to be ready before running tests
# meshfed-api: port 8080 (takes ~60-120s for first start)
# block-coordinator: port 8083
# replicator: port 7080
# manual-block-runner: no HTTP port (check log for "Started BlockRunnerApplicationKt")
until curl -sf http://localhost:8080/mesh/info > /dev/null 2>&1; do sleep 5; done
echo "meshfed-api ready"
until curl -sf http://localhost:8083/actuator/health > /dev/null 2>&1; do sleep 5; done
echo "block-coordinator ready"
until curl -sf http://localhost:7080/actuator/health > /dev/null 2>&1; do sleep 5; done
echo "replicator ready"
until grep -q "Started BlockRunnerApplicationKt" /tmp/manual-runner.log 2>/dev/null; do sleep 5; done
echo "manual-runner ready"
```

**Run tests:**

Individual acceptance tests complete in seconds. The full suite should finish in under 5 minutes.
If tests hang or the 600s timeout is reached, a service is likely down — check logs immediately
rather than waiting for the timeout.

```bash
cd terraform-provider-meshstack/
set -a && source .env && set +a # exports MESHSTACK_ENDPOINT, MESHSTACK_API_KEY, MESHSTACK_API_SECRET
Expand Down Expand Up @@ -472,6 +486,7 @@ go test -count=1 -parallel 4 -timeout 300s ./internal/provider/ 2>&1 | tee /tmp/
- `TF_ACC` must be **unset** — the Terraform Plugin SDK then skips real provider startup and the mock client injected via `SetupMockClient()` is used instead.
- Always use `-parallel 4` since the mock-backed tests run the full acceptance test harness and are slow without it.
- Always tee to a file so errors can be inspected without re-running.
- Mock clients must use **value receivers** (not pointer receivers). This avoids needing to pass pointers when constructing the `client.Client` struct.

**Acceptance tests** (real local meshStack):
```bash
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ BREAKING CHANGES:
- `meshstack_platform` (OpenShift): Remove `enable_template_instantiation` field from the replication config. This field has been removed from the meshStack API.
- `meshstack_landingzone`: Remove `openshift_template` field from OpenShift platform properties. This field has been removed from the meshStack API.

FEATURES:
- New `meshstack_api_key` resource for managing meshStack API keys with automatic secret rotation on expiry change.

## v0.20.5

FEATURES:
Expand Down
61 changes: 61 additions & 0 deletions client/api_key.go
Original file line number Diff line number Diff line change
@@ -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"`
Comment thread
grubmeshi marked this conversation as resolved.
}

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)
}
232 changes: 232 additions & 0 deletions client/api_key_permissions.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading