Skip to content
This repository has been archived by the owner on Dec 3, 2024. It is now read-only.

Commit

Permalink
feat(core): add initial support for actions (#23)
Browse files Browse the repository at this point in the history
* feat: initial support for actions

* docs: update README

* docs: update README again

* fix: avoid lock table errors for now
  • Loading branch information
cyclimse authored Nov 19, 2023
1 parent 48ec0d1 commit e4fb209
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 103 deletions.
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ You can also provide your Scaleway credentials using environment variables. See

## Keybindings

| Keybinding | Description |
|-----------------|-----------------------------------------|
| `esc`, `ctrl+c` | Quit |
| `\` | Search |
| `d` | Describe selected resource |
| `x` | Delete selected resource |
| `l` | View Cockpit logs for selected resource |
| Keybinding | Description |
|-----------------|------------------------------------------|
| `esc`, `ctrl+c` | Quit |
| `\` | Search |
| `d` | Describe selected resource |
| `x` | Delete selected resource |
| `l` | View Cockpit logs for selected resource |
| `t` | View quick actions for selected resource |

## Features

Expand All @@ -52,16 +53,21 @@ You can view the logs for a resource by pressing `l` when it is selected. This w

This feature relies on the Cockpit Loki API. It will generate a token for each project to allow you to view the logs of the resources in the project. As such, you will need to provide the `ObservabilityFullAccess` permission set to your API token.

### Quick Actions

Quick actions are available for some resources. You can view the available actions by pressing `t` when a resource is selected. This will open a new window with the available actions.

## Supported Resources

| Resource | List | Describe | Delete | Logs |
|----------------------|------|----------|--------|------|
| Serverless Function |||||
| Serverless Container |||||
| Registry Namespace |||||
| RDB Instance |||||
| Kapsule Cluster |||||
| Instance |||||
| Resource | List | Describe | Delete | Logs | Actions |
|----------------------|------|----------|--------|------|------------------|
| Serverless Function ||||| (planned) |
| Serverless Container ||||| (planned) |
| Serverless Job ||||| `Start`, `Retry` |
| Registry Namespace ||||| |
| RDB Instance ||||| |
| Kapsule Cluster ||||| |
| Instance ||||| (planned) |

## Troubleshooting

Expand Down
4 changes: 2 additions & 2 deletions internal/discovery/scaleway/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func (d *ResourceDiscover) discoverJobsInRegion(ctx context.Context, region scw.
}

resources = append(resources, scaleway.JobRun{
JobRun: *jobRun,
ProjectID: jobDef.ProjectID,
JobRun: *jobRun,
JobDefinition: *jobDef,
})
}
}
Expand Down
13 changes: 13 additions & 0 deletions internal/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,16 @@ type Resource interface {
// Delete deletes the resource.
Delete(ctx context.Context, s Storer, client *scw.Client) error
}

type Action struct {
// Name is the name of the action.
Name string

// Do performs the action on the resource.
Do func(ctx context.Context, s Storer, client *scw.Client) error
}

type Actionable interface {
// Actions returns the list of actions that can be performed on the resource.
Actions() []Action
}
55 changes: 34 additions & 21 deletions internal/resource/scaleway/job_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,6 @@ func (def JobDefinition) CockpitMetadata() resource.CockpitMetadata {
}
}

func (def JobDefinition) Trigger(ctx context.Context, s resource.Storer, client *scw.Client) error {
api := sdk.NewAPI(client)
run, err := api.StartJobDefinition(&sdk.StartJobDefinitionRequest{
ID: def.ID,
Region: def.Region,
})
if err != nil {
return err
}

err = s.Store(ctx, JobRun{
JobRun: *run,
ProjectID: def.ProjectID,
})
if err != nil {
return err
}

return nil
}

func (def JobDefinition) Delete(ctx context.Context, s resource.Storer, client *scw.Client) error {
api := sdk.NewAPI(client)
err := api.DeleteJobDefinition(&sdk.DeleteJobDefinitionRequest{
Expand All @@ -60,3 +39,37 @@ func (def JobDefinition) Delete(ctx context.Context, s resource.Storer, client *

return s.DeleteResource(ctx, def)
}

func (def JobDefinition) Actions() []resource.Action {
return []resource.Action{
{
Name: "Start",
Do: func(ctx context.Context, s resource.Storer, client *scw.Client) error {
api := sdk.NewAPI(client)
r, err := api.StartJobDefinition(&sdk.StartJobDefinitionRequest{
ID: def.ID,
Region: def.Region,
})
if err != nil {
return err
}

startedRun := &JobRun{
JobRun: *r,
JobDefinition: sdk.JobDefinition(def),
}

err = s.Store(ctx, startedRun)
if err != nil {
return err
}

go func() {
_ = startedRun.pollUntilTerminated(ctx, s, client)
}()

return nil
},
},
}
}
72 changes: 69 additions & 3 deletions internal/resource/scaleway/job_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ package scaleway

import (
"context"
"time"

"github.com/cyclimse/scwtui/internal/resource"
sdk "github.com/scaleway/scaleway-sdk-go/api/jobs/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type JobRun struct {
sdk.JobRun `json:"job_run"`
ProjectID string `json:"project_id"`
sdk.JobRun `json:"job_run"`
JobDefinition sdk.JobDefinition `json:"job_definition"`
}

func (run JobRun) Metadata() resource.Metadata {
return resource.Metadata{
ID: run.ID,
Name: run.ID,
ProjectID: run.ProjectID,
ProjectID: run.JobDefinition.ProjectID,
Status: statusPtr(run.State),
Type: resource.TypeJobRun,
Locality: resource.Region(run.Region),
Expand All @@ -36,3 +37,68 @@ func (run JobRun) CockpitMetadata() resource.CockpitMetadata {
func (run JobRun) Delete(_ context.Context, _ resource.Storer, _ *scw.Client) error {
return nil
}

func (run JobRun) Actions() []resource.Action {
return []resource.Action{
{
Name: "Retry",
Do: func(ctx context.Context, s resource.Storer, client *scw.Client) error {
api := sdk.NewAPI(client)
r, err := api.StartJobDefinition(&sdk.StartJobDefinitionRequest{
ID: run.JobDefinition.ID,
Region: run.JobDefinition.Region,
})
if err != nil {
return err
}

retriedRun := &JobRun{
JobRun: *r,
JobDefinition: run.JobDefinition,
}

err = s.Store(ctx, retriedRun)
if err != nil {
return err
}

go func() {
_ = retriedRun.pollUntilTerminated(ctx, s, client)
}()

return nil
},
},
}
}

func (run JobRun) pollUntilTerminated(ctx context.Context, s resource.Storer, client *scw.Client) error {
api := sdk.NewAPI(client)
for {
r, err := api.GetJobRun(&sdk.GetJobRunRequest{
ID: run.ID,
Region: run.Region,
})
if err != nil {
return err
}

run.JobRun = *r

if err = s.Store(ctx, run); err != nil {
return err
}

if run.State == sdk.JobRunStateSucceeded ||
run.State == sdk.JobRunStateFailed ||
run.State == sdk.JobRunStateCanceled {
return nil
}

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(5 * time.Second):
}
}
}
133 changes: 85 additions & 48 deletions internal/store/sqlite/mutations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,62 +13,99 @@ import (
"github.com/stretchr/testify/require"
)

// nolint:funlen // test multiple methods
func TestStore_Store(t *testing.T) {
ctx := context.Background()

store, err := sqlite.NewStore(ctx, t.TempDir())
require.NoError(t, err)
defer store.Close()

r := &testhelpers.MockResource{
MetadataValue: resource.Metadata{
Name: "name",
ID: "id",
ProjectID: "project-id",
Description: &[]string{"description"}[0],
Tags: []string{"tag1", "tag2"},
Type: resource.TypeFunction,
Locality: resource.Region(scw.RegionFrPar),
},
}

err = store.Store(ctx, r)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, store.Close())
})

// We will directly query the database to check if the resource was correctly stored.
// This is not ideal but it's the only way to check if the resource was correctly stored.
var (
id string
name string
projectID string
description string
tags string
typ int
locality string
data string
)

err = store.DB.QueryRowContext(ctx, "SELECT * FROM resources WHERE id = ?", r.Metadata().ID).Scan(
&id,
&name,
&projectID,
&description,
&tags,
&typ,
&locality,
&data,
)
require.NoError(t, err)
t.Run("store resource that does not exist", func(t *testing.T) {
r := &testhelpers.MockResource{
MetadataValue: resource.Metadata{
Name: "name",
ID: "id",
ProjectID: "project-id",
Description: &[]string{"description"}[0],
Tags: []string{"tag1", "tag2"},
Type: resource.TypeFunction,
Locality: resource.Region(scw.RegionFrPar),
},
}

err = store.Store(ctx, r)
require.NoError(t, err)

// We will directly query the database to check if the resource was correctly stored.
// This is not ideal but it's the only way to check if the resource was correctly stored.
var (
id string
name string
projectID string
description string
tags string
typ int
locality string
data string
)

err = store.DB.QueryRowContext(ctx, "SELECT * FROM resources WHERE id = ?", r.Metadata().ID).Scan(
&id,
&name,
&projectID,
&description,
&tags,
&typ,
&locality,
&data,
)
require.NoError(t, err)

meta := r.Metadata()

assert.Equal(t, meta.ID, id)
assert.Equal(t, meta.Name, name)
assert.Equal(t, meta.ProjectID, projectID)
assert.Equal(t, *meta.Description, description)
assert.Equal(t, meta.Tags, strings.Split(tags, ",")) // tags are stored as a comma separated string
assert.Equal(t, meta.Type, resource.Type(typ))
assert.Equal(t, meta.Locality, resource.Region(scw.RegionFrPar))
})

t.Run("store resource that already exists", func(t *testing.T) {
r := &testhelpers.MockResource{
MetadataValue: resource.Metadata{
ID: "exists",
Locality: resource.Region(scw.RegionFrPar),
},
}

meta := r.Metadata()
err = store.Store(ctx, r)
require.NoError(t, err)

assert.Equal(t, meta.ID, id)
assert.Equal(t, meta.Name, name)
assert.Equal(t, meta.ProjectID, projectID)
assert.Equal(t, *meta.Description, description)
assert.Equal(t, meta.Tags, strings.Split(tags, ",")) // tags are stored as a comma separated string
assert.Equal(t, meta.Type, resource.Type(typ))
assert.Equal(t, meta.Locality, resource.Region(scw.RegionFrPar))
// Update the resource
r.MetadataValue = resource.Metadata{
ID: "exists",
Locality: resource.Region(scw.RegionPlWaw),
}

err = store.Store(ctx, r)
require.NoError(t, err)

// We will directly query the database to check if the resource was correctly stored.
// This is not ideal but it's the only way to check if the resource was correctly stored.
var region string

err = store.DB.QueryRowContext(ctx, "SELECT locality FROM resources WHERE id = ?", r.Metadata().ID).Scan(
&region,
)
require.NoError(t, err)

assert.Equal(t, "pl-waw", region)
})
}

func TestStore_DeleteResource(t *testing.T) {
Expand Down
Loading

0 comments on commit e4fb209

Please sign in to comment.