diff --git a/app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go b/app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go index c3acb8e83..765d6992f 100644 --- a/app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go +++ b/app/controlplane/plugins/core/slack-webhook/v1/slack_webhook.go @@ -96,7 +96,9 @@ func (i *Integration) Execute(_ context.Context, req *sdk.ExecutionRequest) erro return fmt.Errorf("running validation: %w", err) } - summary, err := sdk.SummaryTable(req) + // 4000 is the max size of a Slack message + // if the message is larger than that, we will truncate it + summary, err := sdk.SummaryTable(req, sdk.WithMaxSize(4000)) if err != nil { return fmt.Errorf("error summarizing the request: %w", err) } diff --git a/app/controlplane/plugins/sdk/v1/helpers.go b/app/controlplane/plugins/sdk/v1/helpers.go index 1e48e420d..408e029d5 100644 --- a/app/controlplane/plugins/sdk/v1/helpers.go +++ b/app/controlplane/plugins/sdk/v1/helpers.go @@ -26,8 +26,8 @@ import ( ) type renderer struct { - render func(t table.Writer) string - format string + render func(t table.Writer) string + maxSize int } type RenderOpt func(r *renderer) error @@ -51,7 +51,13 @@ func WithFormat(format string) RenderOpt { return fmt.Errorf("unsupported format %s", format) } - r.format = format + return nil + } +} + +func WithMaxSize(max int) RenderOpt { + return func(r *renderer) error { + r.maxSize = max return nil } } @@ -60,7 +66,7 @@ func newRenderer(opts ...RenderOpt) (*renderer, error) { r := &renderer{ render: func(t table.Writer) string { return t.Render() - }, format: "text", + }, } for _, opt := range opts { @@ -182,11 +188,35 @@ func (r *renderer) summaryTable(m *ChainloopMetadata, predicate chainloop.Normal result += "\n" + r.render(mt) } - result += fmt.Sprintf("\n\nGet Full Attestation\n\n$ chainloop workflow run describe --id %s -o statement", wr.ID) + footer := fmt.Sprintf("\n\nGet Full Attestation\n\n$ chainloop workflow run describe --id %s -o statement", wr.ID) + + // Truncate the text if it's too long to be displayed, the footer will be kept + if r.maxSize > 0 { + result = truncateText(result, r.maxSize-len(footer)) + } + + result += footer return result, nil } +// Truncate returns the first n runes of s. +func truncateText(s string, n int) string { + truncatedPrefix := "... (truncated)" + n -= len(truncatedPrefix) + + if len(s) <= n { + return s + } + for i := range s { + if n == 0 { + return s[:i] + truncatedPrefix + } + n-- + } + return s +} + func SummaryTable(req *ExecutionRequest, opts ...RenderOpt) (string, error) { renderer, err := newRenderer(opts...) if err != nil { diff --git a/app/controlplane/plugins/sdk/v1/helpers_test.go b/app/controlplane/plugins/sdk/v1/helpers_test.go index 33c401b56..0484fff73 100644 --- a/app/controlplane/plugins/sdk/v1/helpers_test.go +++ b/app/controlplane/plugins/sdk/v1/helpers_test.go @@ -41,6 +41,12 @@ func (s *helperTestSuite) TestSummaryTable() { inputPath: "testdata/attestations/full.json", outputPath: "testdata/attestations/full.txt", }, + { + name: "truncated text", + inputPath: "testdata/attestations/full.json", + renderOpts: []RenderOpt{WithMaxSize(2000)}, + outputPath: "testdata/attestations/truncated.txt", + }, { name: "full markdown", inputPath: "testdata/attestations/full.json", diff --git a/app/controlplane/plugins/sdk/v1/testdata/attestations/truncated.txt b/app/controlplane/plugins/sdk/v1/testdata/attestations/truncated.txt new file mode 100644 index 000000000..f4f432809 --- /dev/null +++ b/app/controlplane/plugins/sdk/v1/testdata/attestations/truncated.txt @@ -0,0 +1,37 @@ +┌─────────────────────────────────────┐ +│ Workflow │ +├──────────────┬──────────────────────┤ +│ ID │ deadbeef │ +│ Name │ test-workflow │ +│ Team │ test-team │ +│ Project │ test-project │ +├──────────────┼──────────────────────┤ +│ Workflow Run │ │ +├──────────────┼──────────────────────┤ +│ ID │ beefdead │ +│ Started At │ 22 Nov 21 00:00 UTC │ +│ Finished At │ 22 Nov 21 00:10 UTC │ +│ State │ success │ +│ Runner Link │ chainloop.dev/runner │ +│ Annotations │ ------ │ +│ │ branch: stable │ +│ │ toplevel: true │ +└──────────────┴──────────────────────┘ +┌────────────────────────────────────────────────────────────────────────────────┐ +│ Materials │ +├─────────────┬──────────────────────────────────────────────────────────────────┤ +│ Name │ image │ +│ Type │ CONTAINER_IMAGE │ +│ Value │ index.docker.io/bitnami/nginx │ +│ Digest │ 264f55a6ff9cec2f4742a9faacc033b29f65c04dd4480e71e23579d484288d61 │ +├─────────────┼──────────────────────────────────────────────────────────────────┤ +│ Name │ skynet-sbom │ +│ Type │ SBOM_CYCLONEDX_JSON │ +│ Value │ sbom.cyclonedx.json │ +│ Digest │ 16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c │ +│ Annotations │ ------ │ +│ │ component: nginx ... (truncated) + +Get Full Attestation + +$ chainloop workflow run describe --id beefdead -o statement \ No newline at end of file