diff --git a/README.md b/README.md index 2234be5..e36d0a6 100644 --- a/README.md +++ b/README.md @@ -256,29 +256,30 @@ Press `Esc` to return to the previous resource and selection. ## Currently Supported Resources -| Service | Resources | Status | -|---|---|:-:| -| **Local / Auth** | contexts | Partial | -| **Keystone / Identity** | domains, projects, users, groups, roles | ✅ | -| **Keystone / Catalog** | catalog, services, endpoints, regions | ✅ | -| **Cinder / Block Storage** | volumes, snapshots, volume types, volume backups | ✅ | -| **Glance / Image** | images | ✅ | -| **Swift / Object Storage** | swift containers, swift objects | ✅ | -| **Nova / Compute** | servers, flavors | ✅ | -| **Nova / Compute** | keypairs, server groups, availability zones | ✅ | -| **Nova / Compute** | hypervisors, host aggregates | ✅ | -| **Neutron / Network** | networks, subnets, ports, routers, floating IPs, security groups | ✅ | -| **Neutron / Network** | security group rules | ✅ | -| **Neutron / Network** | router interfaces | ⬜ | -| **Designate / DNS** | zones, recordsets | ✅ | -| **Heat / Orchestration** | stacks, stack resources, stack events | ✅ | -| **Octavia / Load Balancing** | load balancers, listeners, pools, members | ✅ | -| **Octavia / Load Balancing** | health monitors, L7 policies, L7 rules | ✅ | -| **Barbican / Key Manager** | secrets, secret containers, orders | ✅ | -| **Manila / Shared File Systems** | shares, share snapshots, share networks, share types | ✅ | -| **Manila / Shared File Systems** | share replicas, access rules, export locations | ✅ | -| **Magnum / Container Infrastructure** | clusters, cluster templates, node groups | ✅ | -| **Trove / Databases** | instances, databases, users, backups, datastores | ✅ | +| Service | Resources | Status | +|---------------------------------------|------------------------------------------------------------------|:-------:| +| **Local / Auth** | contexts | Partial | +| **Keystone / Identity** | domains, projects, users, groups, roles | ✅ | +| **Keystone / Catalog** | catalog, services, endpoints, regions | ✅ | +| **Cinder / Block Storage** | volumes, snapshots, volume types, volume backups | ✅ | +| **Glance / Image** | images | ✅ | +| **Swift / Object Storage** | swift containers, swift objects | ✅ | +| **Nova / Compute** | servers, flavors | ✅ | +| **Nova / Compute** | keypairs, server groups, availability zones | ✅ | +| **Nova / Compute** | hypervisors, host aggregates | ✅ | +| **Neutron / Network** | networks, subnets, ports, routers, floating IPs, security groups | ✅ | +| **Neutron / Network** | security group rules | ✅ | +| **Neutron / Network** | router interfaces | ⬜ | +| **Designate / DNS** | zones, recordsets | ✅ | +| **Heat / Orchestration** | stacks, stack resources, stack events | ✅ | +| **Heat / Orchestration** | software deployments | ✅ | +| **Octavia / Load Balancing** | load balancers, listeners, pools, members | ✅ | +| **Octavia / Load Balancing** | health monitors, L7 policies, L7 rules | ✅ | +| **Barbican / Key Manager** | secrets, secret containers, orders | ✅ | +| **Manila / Shared File Systems** | shares, share snapshots, share networks, share types | ✅ | +| **Manila / Shared File Systems** | share replicas, access rules, export locations | ✅ | +| **Magnum / Container Infrastructure** | clusters, cluster templates, node groups | ✅ | +| **Trove / Databases** | instances, databases, users, backups, datastores | ✅ | > [!CAUTION] > **o7k is early beta and under active development.** Expect bugs, incomplete features, and breaking changes. Behavior and configuration may change without notice. @@ -287,70 +288,71 @@ Press `Esc` to return to the previous resource and selection. ### Aliases -| Service | Resource | Command | Aliases | -|---|---|---|---| -| Command Palette | Aliases | `commands` | `command`, `cmd`, `aliases`, `alias` | -| Local / Auth | Contexts | `contexts` | `context`, `ctx`, `cloud`, `clouds` | -| Keystone / Catalog | Catalog | `catalog` | `cat` | -| Keystone / Catalog | Services | `services` | `service`, `svc` | -| Keystone / Catalog | Endpoints | `endpoints` | `endpoint` | -| Keystone / Catalog | Regions | `regions` | `region` | -| Keystone / Identity | Domains | `domains` | `domain` | -| Keystone / Identity | Projects | `projects` | `project` | -| Keystone / Identity | Users | `users` | `user` | -| Keystone / Identity | Groups | `groups` | `group` | -| Keystone / Identity | Roles | `roles` | `role` | -| Nova / Compute | Servers | `servers` | `server`, `srv` | -| Nova / Compute | Flavors | `flavors` | `flavor` | -| Nova / Compute | Keypairs | `keypairs` | `keypair`, `keys`, `key` | -| Nova / Compute | Server Groups | `servergroups` | `servergroup`, `sgroups`, `sgroup` | -| Nova / Compute | Availability Zones | `availabilityzones` | `availabilityzone`, `azs`, `az` | -| Nova / Compute | Hypervisors | `hypervisors` | `hypervisor`, `hyp`, `hyps` | -| Nova / Compute | Host Aggregates | `aggregates` | `aggregate`, `hostaggregates`, `hostaggregate` | -| Neutron / Network | Networks | `networks` | `network`, `net` | -| Neutron / Network | Subnets | `subnets` | `subnet` | -| Neutron / Network | Ports | `ports` | `port` | -| Neutron / Network | Routers | `routers` | `router` | -| Neutron / Network | Floating IPs | `floatingips` | `floatingip`, `fips`, `fip` | -| Neutron / Network | Security Groups | `securitygroups` | `securitygroup`, `secgroups`, `secgroup`, `sg` | -| Neutron / Network | Security Group Rules | `securitygrouprules` | `securitygrouprule`, `secrules`, `secrule`, `sgr` | -| Cinder / Block Storage | Volumes | `volumes` | `volume`, `vol` | -| Cinder / Block Storage | Snapshots | `snapshots` | `snapshot`, `snap` | -| Cinder / Block Storage | Volume Types | `volumetypes` | `volumetype` | -| Cinder / Block Storage | Volume Backups | `backups` | `backup`, `volume-backups`, `volume-backup` | -| Glance / Image | Images | `images` | `image`, `img` | -| Swift / Object Storage | Swift Containers | `swift-containers` | `swift-container` | -| Swift / Object Storage | Swift Objects | `swift-objects` | `swift-object` | -| Heat / Orchestration | Stacks | `stacks` | `stack` | -| Heat / Orchestration | Stack Resources | `stack-resources` | `stack-resource` | -| Heat / Orchestration | Stack Events | `stack-events` | `stack-event` | -| Octavia / Load Balancing | Load Balancers | `loadbalancers` | `loadbalancer`, `lbs`, `lb` | -| Octavia / Load Balancing | Listeners | `listeners` | `listener` | -| Octavia / Load Balancing | Pools | `pools` | `pool` | -| Octavia / Load Balancing | Members | `members` | `member` | -| Octavia / Load Balancing | Health Monitors | `healthmonitors` | `healthmonitor`, `monitors`, `monitor` | -| Octavia / Load Balancing | L7 Policies | `l7policies` | `l7policy`, `l7-policies`, `l7-policy` | -| Octavia / Load Balancing | L7 Rules | `l7rules` | `l7rule`, `l7-rules`, `l7-rule` | -| Barbican / Key Manager | Secrets | `secrets` | `secret` | -| Barbican / Key Manager | Secret Containers | `secret-containers` | `secret-container` | -| Barbican / Key Manager | Orders | `orders` | `order` | -| Manila / Shared File Systems | Shares | `shares` | `share` | -| Manila / Shared File Systems | Share Snapshots | `share-snapshots` | `share-snapshot` | -| Manila / Shared File Systems | Share Replicas | `share-replicas` | `share-replica` | -| Manila / Shared File Systems | Share Networks | `share-networks` | `share-network` | -| Manila / Shared File Systems | Share Types | `share-types` | `share-type` | -| Manila / Shared File Systems | Share Access Rules | `share-access-rules` | `share-access-rule` | -| Manila / Shared File Systems | Share Export Locations | `share-export-locations` | `share-export-location` | -| Magnum / Container Infrastructure | Clusters | `clusters` | `cluster`, `coe` | -| Magnum / Container Infrastructure | Cluster Templates | `clustertemplates` | `clustertemplate` | -| Magnum / Container Infrastructure | Node Groups | `nodegroups` | `nodegroup` | -| Designate / DNS | Zones | `zones` | `zone`, `dns-zones`, `dns-zone` | -| Designate / DNS | Recordsets | `recordsets` | `recordset`, `records`, `record` | -| Trove / Databases | Instances | `db-instances` | `db-instance`, `trove` | -| Trove / Databases | Databases | `db-databases` | `db-database` | -| Trove / Databases | Users | `db-users` | `db-user` | -| Trove / Databases | Backups | `db-backups` | `db-backup` | -| Trove / Databases | Datastores | `db-datastores` | `db-datastore` | +| Service | Resource | Command | Aliases | +|-----------------------------------|------------------------|--------------------------|---------------------------------------------------| +| Command Palette | Aliases | `commands` | `command`, `cmd`, `aliases`, `alias` | +| Local / Auth | Contexts | `contexts` | `context`, `ctx`, `cloud`, `clouds` | +| Keystone / Catalog | Catalog | `catalog` | `cat` | +| Keystone / Catalog | Services | `services` | `service`, `svc` | +| Keystone / Catalog | Endpoints | `endpoints` | `endpoint` | +| Keystone / Catalog | Regions | `regions` | `region` | +| Keystone / Identity | Domains | `domains` | `domain` | +| Keystone / Identity | Projects | `projects` | `project` | +| Keystone / Identity | Users | `users` | `user` | +| Keystone / Identity | Groups | `groups` | `group` | +| Keystone / Identity | Roles | `roles` | `role` | +| Nova / Compute | Servers | `servers` | `server`, `srv` | +| Nova / Compute | Flavors | `flavors` | `flavor` | +| Nova / Compute | Keypairs | `keypairs` | `keypair`, `keys`, `key` | +| Nova / Compute | Server Groups | `servergroups` | `servergroup`, `sgroups`, `sgroup` | +| Nova / Compute | Availability Zones | `availabilityzones` | `availabilityzone`, `azs`, `az` | +| Nova / Compute | Hypervisors | `hypervisors` | `hypervisor`, `hyp`, `hyps` | +| Nova / Compute | Host Aggregates | `aggregates` | `aggregate`, `hostaggregates`, `hostaggregate` | +| Neutron / Network | Networks | `networks` | `network`, `net` | +| Neutron / Network | Subnets | `subnets` | `subnet` | +| Neutron / Network | Ports | `ports` | `port` | +| Neutron / Network | Routers | `routers` | `router` | +| Neutron / Network | Floating IPs | `floatingips` | `floatingip`, `fips`, `fip` | +| Neutron / Network | Security Groups | `securitygroups` | `securitygroup`, `secgroups`, `secgroup`, `sg` | +| Neutron / Network | Security Group Rules | `securitygrouprules` | `securitygrouprule`, `secrules`, `secrule`, `sgr` | +| Cinder / Block Storage | Volumes | `volumes` | `volume`, `vol` | +| Cinder / Block Storage | Snapshots | `snapshots` | `snapshot`, `snap` | +| Cinder / Block Storage | Volume Types | `volumetypes` | `volumetype` | +| Cinder / Block Storage | Volume Backups | `backups` | `backup`, `volume-backups`, `volume-backup` | +| Glance / Image | Images | `images` | `image`, `img` | +| Swift / Object Storage | Swift Containers | `swift-containers` | `swift-container` | +| Swift / Object Storage | Swift Objects | `swift-objects` | `swift-object` | +| Heat / Orchestration | Stacks | `stacks` | `stack` | +| Heat / Orchestration | Stack Resources | `stack-resources` | `stack-resource` | +| Heat / Orchestration | Stack Events | `stack-events` | `stack-event` | +| Heat / Orchestration | Software Deployments | `software-deployments` | `software-deployment` | +| Octavia / Load Balancing | Load Balancers | `loadbalancers` | `loadbalancer`, `lbs`, `lb` | +| Octavia / Load Balancing | Listeners | `listeners` | `listener` | +| Octavia / Load Balancing | Pools | `pools` | `pool` | +| Octavia / Load Balancing | Members | `members` | `member` | +| Octavia / Load Balancing | Health Monitors | `healthmonitors` | `healthmonitor`, `monitors`, `monitor` | +| Octavia / Load Balancing | L7 Policies | `l7policies` | `l7policy`, `l7-policies`, `l7-policy` | +| Octavia / Load Balancing | L7 Rules | `l7rules` | `l7rule`, `l7-rules`, `l7-rule` | +| Barbican / Key Manager | Secrets | `secrets` | `secret` | +| Barbican / Key Manager | Secret Containers | `secret-containers` | `secret-container` | +| Barbican / Key Manager | Orders | `orders` | `order` | +| Manila / Shared File Systems | Shares | `shares` | `share` | +| Manila / Shared File Systems | Share Snapshots | `share-snapshots` | `share-snapshot` | +| Manila / Shared File Systems | Share Replicas | `share-replicas` | `share-replica` | +| Manila / Shared File Systems | Share Networks | `share-networks` | `share-network` | +| Manila / Shared File Systems | Share Types | `share-types` | `share-type` | +| Manila / Shared File Systems | Share Access Rules | `share-access-rules` | `share-access-rule` | +| Manila / Shared File Systems | Share Export Locations | `share-export-locations` | `share-export-location` | +| Magnum / Container Infrastructure | Clusters | `clusters` | `cluster`, `coe` | +| Magnum / Container Infrastructure | Cluster Templates | `clustertemplates` | `clustertemplate` | +| Magnum / Container Infrastructure | Node Groups | `nodegroups` | `nodegroup` | +| Designate / DNS | Zones | `zones` | `zone`, `dns-zones`, `dns-zone` | +| Designate / DNS | Recordsets | `recordsets` | `recordset`, `records`, `record` | +| Trove / Databases | Instances | `db-instances` | `db-instance`, `trove` | +| Trove / Databases | Databases | `db-databases` | `db-database` | +| Trove / Databases | Users | `db-users` | `db-user` | +| Trove / Databases | Backups | `db-backups` | `db-backup` | +| Trove / Databases | Datastores | `db-datastores` | `db-datastore` | ## Development diff --git a/cmd/main.go b/cmd/main.go index 3cac593..b3e29ab 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -66,6 +66,7 @@ import ( "github.com/akyriako/o7k/internal/resources/networking/subnets" swiftcontainers "github.com/akyriako/o7k/internal/resources/objectstorage/containers" swiftobjects "github.com/akyriako/o7k/internal/resources/objectstorage/objects" + "github.com/akyriako/o7k/internal/resources/orchestration/softwaredeployments" "github.com/akyriako/o7k/internal/resources/orchestration/stackevents" "github.com/akyriako/o7k/internal/resources/orchestration/stackresources" "github.com/akyriako/o7k/internal/resources/orchestration/stacks" @@ -456,6 +457,10 @@ func registerAll(r *resource.Registry, openstackContext *openstack.Context) (err errs = errors.Join(errs, fmt.Errorf("registering stacks resources: %w", err)) } + if err := r.Register(softwaredeployments.New(openstackContext)); err != nil { + errs = errors.Join(errs, fmt.Errorf("registering stacks resource: %w", err)) + } + if err := r.Register(zones.New(openstackContext)); err != nil { errs = errors.Join(errs, fmt.Errorf("registering dns zones resource: %w", err)) } diff --git a/go.mod b/go.mod index dd02dee..871fba6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/ansi v0.11.6 + github.com/dc9-dev/promptveil v0.0.0-20260914101915-1cf7b430f7a8 github.com/gophercloud/gophercloud/v2 v2.14.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.8.0 diff --git a/go.sum b/go.sum index 44e39ff..f937d98 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dc9-dev/promptveil v0.0.0-20260914101915-1cf7b430f7a8 h1:FZCEwbfqRR8oOBnIsfaNOaT/QM8DefOdnzT1n8hHTx0= +github.com/dc9-dev/promptveil v0.0.0-20260914101915-1cf7b430f7a8/go.mod h1:PScsCB4AI1DHxgKWUrGN3DMsIwIWSAnMkz70qw0gFcM= github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= diff --git a/internal/resource/commands.go b/internal/resource/commands.go index e8be9a6..f3636be 100644 --- a/internal/resource/commands.go +++ b/internal/resource/commands.go @@ -18,9 +18,10 @@ type NavigateFilteredMultiMsg struct { } type DetailsMsg struct { - ID string - Content any - Err error + ID string + Content any + Err error + WrapContent *bool } type NavigateScopedMsg struct { diff --git a/internal/resources/orchestration/softwaredeployments/commands.go b/internal/resources/orchestration/softwaredeployments/commands.go new file mode 100644 index 0000000..ba37986 --- /dev/null +++ b/internal/resources/orchestration/softwaredeployments/commands.go @@ -0,0 +1,116 @@ +package softwaredeployments + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/akyriako/o7k/internal/resource" + tea "github.com/charmbracelet/bubbletea" + "github.com/dc9-dev/promptveil" +) + +func (r *Resource) show(row resource.Row) tea.Cmd { + return func() tea.Msg { + client, err := r.context.OrchestrationV1() + if err != nil { + return resource.DetailsMsg{Err: err} + } + + var result struct { + Deployment Deployment `json:"software_deployment"` + } + + _, err = client.Get(context.Background(), client.ServiceURL("software_deployments", row.ID), &result, nil) + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("getting software deployment %q: %w", row.ID, err)} + } + + return resource.DetailsMsg{ + ID: result.Deployment.ID, + Content: result.Deployment, + } + } +} + +func (r *Resource) outputs(row resource.Row) tea.Cmd { + return func() tea.Msg { + client, err := r.context.OrchestrationV1() + if err != nil { + return resource.DetailsMsg{Err: err} + } + + var result struct { + SoftwareDeployment struct { + OutputValues map[string]any `json:"output_values"` + } `json:"software_deployment"` + } + + _, err = client.Get(context.Background(), client.ServiceURL("software_deployments", row.ID), &result, nil) + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("getting software deployment outputs %q: %w", row.ID, err)} + } + + outputs := result.SoftwareDeployment.OutputValues + + content := fmt.Sprintf( + "deploy_stdout:\n%s\n\ndeploy_stderr:\n%s\n\ndeploy_status_code: %v", + outputs["deploy_stdout"], + outputs["deploy_stderr"], + outputs["deploy_status_code"], + ) + + redacted := promptveil.Redact(content) + return resource.DetailsMsg{ + ID: row.ID, + Content: redacted.Text, + WrapContent: new(true), + } + } +} + +func (r *Resource) config(row resource.Row) tea.Cmd { + return func() tea.Msg { + client, err := r.context.OrchestrationV1() + if err != nil { + return resource.DetailsMsg{Err: err} + } + + var deployment struct { + SoftwareDeployment struct { + ConfigID string `json:"config_id"` + } `json:"software_deployment"` + } + + _, err = client.Get(context.Background(), client.ServiceURL("software_deployments", row.ID), &deployment, nil) + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("getting software deployment %q: %w", row.ID, err)} + } + + configID := deployment.SoftwareDeployment.ConfigID + if configID == "" { + return resource.DetailsMsg{Err: fmt.Errorf("software deployment %q has no config_id", row.ID)} + } + + var result struct { + SoftwareConfig map[string]any `json:"software_config"` + } + + _, err = client.Get(context.Background(), client.ServiceURL("software_configs", configID), &result, nil) + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("getting software config %q: %w", configID, err)} + } + + content, err := json.MarshalIndent(result.SoftwareConfig, "", " ") + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("marshalling software config %q: %w", configID, err)} + } + + redacted := promptveil.Redact(string(content)) + return resource.DetailsMsg{ + ID: configID, + Content: redacted.Text, + WrapContent: new(true), + } + } +} diff --git a/internal/resources/orchestration/softwaredeployments/softwaredeployments.go b/internal/resources/orchestration/softwaredeployments/softwaredeployments.go new file mode 100644 index 0000000..e31ea4e --- /dev/null +++ b/internal/resources/orchestration/softwaredeployments/softwaredeployments.go @@ -0,0 +1,105 @@ +package softwaredeployments + +import ( + "context" + "fmt" + + "github.com/akyriako/o7k/internal/openstack" + "github.com/akyriako/o7k/internal/resource" + tea "github.com/charmbracelet/bubbletea" +) + +type Resource struct { + context *openstack.Context +} + +func New(context *openstack.Context) *Resource { + return &Resource{context: context} +} + +func (r *Resource) Title() string { + return "Heat Software Deployments" +} + +func (r *Resource) Kind() string { + return "software-deployments" +} + +func (r *Resource) Aliases() []string { + return []string{"software-deployment"} +} + +func (r *Resource) Columns() []resource.Column { + return []resource.Column{ + {Key: "id", Title: "ID", MinWidth: 36, Flex: 0}, + {Key: "config_id", Title: "CONFIG ID", MinWidth: 36, Flex: 0}, + {Key: "server_id", Title: "SERVER ID", MinWidth: 36, Flex: 0}, + {Key: "action", Title: "ACTION", MinWidth: 6, Flex: 2}, + {Key: "status", Title: "STATUS", MinWidth: 6, Flex: 2}, + {Key: "status_reason", Title: "REASON", MinWidth: 21, Flex: 1}, + } +} + +func (r *Resource) Commands() []resource.Command { + return []resource.Command{ + {Key: "s", Description: "Show", Default: true}, + {Key: "o", Description: "Outputs"}, + {Key: "i", Description: "Config"}, + } +} + +type Deployment struct { + ID string `json:"id"` + ConfigID string `json:"config_id"` + ServerID string `json:"server_id"` + Action string `json:"action"` + Status string `json:"status"` + StatusReason string `json:"status_reason"` + CreationTime string `json:"creation_time"` +} + +func (r *Resource) List(ctx context.Context) ([]resource.Row, error) { + client, err := r.context.OrchestrationV1() + if err != nil { + return nil, err + } + + var result struct { + Deployments []Deployment `json:"software_deployments"` + } + + _, err = client.Get(ctx, client.ServiceURL("software_deployments"), &result, nil) + if err != nil { + return nil, fmt.Errorf("listing software deployments: %w", err) + } + + rows := make([]resource.Row, 0, len(result.Deployments)) + for _, deployment := range result.Deployments { + rows = append(rows, resource.Row{ + ID: deployment.ID, + Fields: map[string]string{ + "id": deployment.ID, + "config_id": deployment.ConfigID, + "server_id": deployment.ServerID, + "action": deployment.Action, + "status": deployment.Status, + "status_reason": deployment.StatusReason, + }, + }) + } + + return rows, nil +} + +func (r *Resource) Execute(command resource.Command, row resource.Row) tea.Cmd { + switch command.Key { + case "s": + return r.show(row) + case "o": + return r.outputs(row) + case "i": + return r.config(row) + } + + return nil +} diff --git a/internal/resources/orchestration/stacks/commands.go b/internal/resources/orchestration/stacks/commands.go index 2e86a45..1b6e1ad 100644 --- a/internal/resources/orchestration/stacks/commands.go +++ b/internal/resources/orchestration/stacks/commands.go @@ -2,10 +2,13 @@ package stacks import ( "context" + "encoding/json" "fmt" "github.com/akyriako/o7k/internal/resource" tea "github.com/charmbracelet/bubbletea" + "github.com/dc9-dev/promptveil" + "github.com/gophercloud/gophercloud/v2/openstack/orchestration/v1/stackresources" orchestrationstacks "github.com/gophercloud/gophercloud/v2/openstack/orchestration/v1/stacks" ) @@ -58,3 +61,61 @@ func (r *Resource) events(row resource.Row) tea.Cmd { } } } + +func (r *Resource) deployments(row resource.Row) tea.Cmd { + return func() tea.Msg { + client, err := r.context.OrchestrationV1() + if err != nil { + return resource.ErrorMsg{Err: err} + } + + pages, err := stackresources.List(client, row.Fields["name"], row.ID, &stackresources.ListOpts{Depth: 99}).AllPages(context.Background()) + if err != nil { + return resource.ErrorMsg{Err: fmt.Errorf("listing stack resources: %w", err)} + } + + items, err := stackresources.ExtractResources(pages) + if err != nil { + return resource.ErrorMsg{Err: fmt.Errorf("extracting stack resources: %w", err)} + } + + ids := make([]string, 0) + for _, item := range items { + if item.Type == "OS::Heat::SoftwareDeployment" && item.PhysicalID != "" { + ids = append(ids, item.PhysicalID) + } + } + + return resource.NavigateFilteredMultiMsg{ + Resource: "software-deployments", + Field: "id", + Values: ids, + } + } +} + +func (r *Resource) environment(row resource.Row) tea.Cmd { + return func() tea.Msg { + client, err := r.context.OrchestrationV1() + if err != nil { + return resource.DetailsMsg{Err: err} + } + + var environment map[string]any + _, err = client.Get(context.Background(), client.ServiceURL("stacks", row.Fields["name"], row.ID, "environment"), &environment, nil) + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("getting stack environment %q: %w", row.ID, err)} + } + + content, err := json.MarshalIndent(environment, "", " ") + if err != nil { + return resource.DetailsMsg{Err: fmt.Errorf("marshalling stack environment %q: %w", row.ID, err)} + } + + redacted := promptveil.Redact(string(content)) + return resource.DetailsMsg{ + ID: row.ID, + Content: redacted.Text, + } + } +} diff --git a/internal/resources/orchestration/stacks/stacks.go b/internal/resources/orchestration/stacks/stacks.go index c9d5b19..f2d9dd0 100644 --- a/internal/resources/orchestration/stacks/stacks.go +++ b/internal/resources/orchestration/stacks/stacks.go @@ -46,6 +46,8 @@ func (r *Resource) Commands() []resource.Command { {Key: "s", Description: "Show"}, {Key: "shift-r", Description: "Resources"}, {Key: "shift-e", Description: "Events", Default: true}, + {Key: "shift-d", Description: "Deployments"}, + {Key: "e", Description: "Environment"}, } } func (r *Resource) List(ctx context.Context) ([]resource.Row, error) { @@ -89,7 +91,10 @@ func (r *Resource) Execute(command resource.Command, row resource.Row) tea.Cmd { return r.resources(row) case "shift-e": return r.events(row) - + case "shift-d": + return r.deployments(row) + case "e": + return r.environment(row) } return nil diff --git a/internal/ui/app.go b/internal/ui/app.go index 81a5ba6..f9bdd95 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -76,6 +76,7 @@ type Model struct { detailID string detailContent string detailContentIsJson bool + detailContentWrap bool detailLabel string commandMode bool @@ -259,6 +260,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "esc" { if m.detailMode { m.detailContentIsJson = false + m.detailContentWrap = false m.detailMode = false m.detailContent = "" m.detailID = "" @@ -613,12 +615,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var displayContent string isJson := true + wrap := false switch content := msg.Content.(type) { case string: m.detailContent = content displayContent = content isJson = false + wrap = true default: data, err := json.MarshalIndent(content, "", " ") @@ -631,7 +635,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { displayContent = colorizeJSON(data) } + if msg.WrapContent != nil { + wrap = *msg.WrapContent + } + m.detailContentIsJson = isJson + m.detailContentWrap = wrap m.detailMode = true m.detailID = msg.ID m.detailViewport.SetContent(displayContent) diff --git a/internal/ui/helpers.go b/internal/ui/helpers.go index e413811..6b282dd 100644 --- a/internal/ui/helpers.go +++ b/internal/ui/helpers.go @@ -168,11 +168,36 @@ func preserveBackground(content string, background lipgloss.Color) string { }) } +//func (m *Model) wrapDetailContent() { +// content := m.detailContent +// +// if m.detailContentIsJson { +// m.detailViewport.SetContent(colorizeJSON([]byte(content))) +// +// if !m.detailContentWrap { +// return +// } +// } +// +// wrapped := lipgloss.NewStyle(). +// Width(max(m.detailViewport.Width, 1)). +// Render(content) +// +// if m.detailContentIsJson { +// m.detailViewport.SetContent(colorizeJSON([]byte(wrapped))) +// } +// +// m.detailViewport.SetContent(wrapped) +//} + func (m *Model) wrapDetailContent() { content := m.detailContent - if m.detailContentIsJson { - m.detailViewport.SetContent(colorizeJSON([]byte(content))) + if !m.detailContentWrap { + if m.detailContentIsJson { + content = colorizeJSON([]byte(content)) + } + m.detailViewport.SetContent(content) return } @@ -180,5 +205,9 @@ func (m *Model) wrapDetailContent() { Width(max(m.detailViewport.Width, 1)). Render(content) + if m.detailContentIsJson { + wrapped = colorizeJSON([]byte(wrapped)) + } + m.detailViewport.SetContent(wrapped) }