Skip to content

Commit beb1996

Browse files
committed
slack: teach ns to post updates to Slack.
1 parent f6b1800 commit beb1996

File tree

5 files changed

+150
-1
lines changed

5 files changed

+150
-1
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ require (
8787
github.com/protocolbuffers/txtpbfmt v0.0.0-20220608084003-fc78c767cd6a
8888
github.com/rs/zerolog v1.20.0
8989
github.com/sirupsen/logrus v1.9.3
90+
github.com/slack-go/slack v0.12.3
9091
github.com/soheilhy/cmux v0.1.5
9192
github.com/spf13/cobra v1.7.0
9293
github.com/spf13/pflag v1.0.5

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
411411
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
412412
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
413413
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
414+
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
415+
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
414416
github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
415417
github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0=
416418
github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY=
@@ -974,6 +976,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
974976
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
975977
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
976978
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
979+
github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88=
980+
github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
977981
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
978982
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
979983
github.com/spdx/tools-golang v0.5.1 h1:fJg3SVOGG+eIva9ZUBm/hvyA7PIPVFjRxUKe6fdAgwE=

internal/cli/fncobra/ns/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ func DoMain(name string, autoUpdate bool, registerCommands func(*cobra.Command))
225225
"If set to true, ns uses the incluster orchestrator for deployment.")
226226
rootCmd.PersistentFlags().BoolVar(&orchestration.RenderOrchestratorDeployment, "render_orchestrator_deployment", orchestration.RenderOrchestratorDeployment,
227227
"If set to true, we print a render wait block while deploying the orchestrator itself.")
228+
rootCmd.PersistentFlags().StringVar(&orchestration.SlackToken, "slack_token", "",
229+
"Token used to call Slack.")
230+
rootCmd.PersistentFlags().StringVar(&orchestration.DeployUpdateSlackChannel, "deploy_update_slack_channel", "",
231+
"Slack channel to send deployment notifications to.")
228232
rootCmd.PersistentFlags().BoolVar(&gcloud.UseHostGCloudBinary, "gcloud_use_host_binary", gcloud.UseHostGCloudBinary,
229233
"If set to true, uses a gcloud binary that is available at the host, rather than ns's builtin.")
230234
rootCmd.PersistentFlags().BoolVar(&filewatcher.FileWatcherUsePolling, "filewatcher_use_polling",
@@ -267,6 +271,8 @@ func DoMain(name string, autoUpdate bool, registerCommands func(*cobra.Command))
267271
"filewatcher_use_polling",
268272
"k3d_ignore_docker_version",
269273
"kubernetes_force_apply",
274+
"slack_token",
275+
"slack_channel",
270276
// Hidden for M0
271277
"testing_use_namespace_cloud",
272278
"testing_use_namespace_cloud_build",

orchestration/migration.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ package orchestration
77
import (
88
"context"
99
"fmt"
10+
"os"
11+
"time"
1012

13+
"github.com/slack-go/slack"
1114
"namespacelabs.dev/foundation/internal/console"
1215
"namespacelabs.dev/foundation/internal/fnerrors"
1316
"namespacelabs.dev/foundation/internal/planning/deploy"
@@ -24,6 +27,7 @@ import (
2427
const orchestratorVersion = 25
2528

2629
var DeployWithOrchestrator = false
30+
var DeployUpdateSlackChannel, SlackToken string
2731

2832
func ExecuteOpts() execution.ExecuteOpts {
2933
return execution.ExecuteOpts{
@@ -38,14 +42,36 @@ func Deploy(ctx context.Context, env cfg.Context, cluster runtime.ClusterNamespa
3842
return fnerrors.BadInputError("waiting is mandatory without the orchestrator")
3943
}
4044

45+
observeError := func(context.Context, error) {}
46+
if DeployUpdateSlackChannel != "" {
47+
if SlackToken == "" {
48+
return fnerrors.BadInputError("a slack token is required to be able to update a channel")
49+
}
50+
51+
start := time.Now()
52+
slackcli := slack.New(os.ExpandEnv(SlackToken))
53+
chid, ts, err := slackcli.PostMessageContext(ctx, os.ExpandEnv(DeployUpdateSlackChannel), slack.MsgOptionBlocks(renderSlackMessage(plan, start, time.Time{}, nil)...))
54+
if err != nil {
55+
fmt.Fprintf(console.Warnings(ctx), "Failed to post to Slack: %v\n", err)
56+
} else {
57+
observeError = func(ctx context.Context, err error) {
58+
if _, _, _, err := slackcli.UpdateMessageContext(ctx, chid, ts, slack.MsgOptionBlocks(renderSlackMessage(plan, start, time.Now(), err)...)); err != nil {
59+
fmt.Fprintf(console.Warnings(ctx), "Failed to update Slack: %v\n", err)
60+
}
61+
}
62+
}
63+
}
64+
4165
p := execution.NewPlan(plan.Program.Invocation...)
4266

4367
// Make sure that the cluster is accessible to a serialized invocation implementation.
44-
return execution.ExecuteExt(ctx, "deployment.execute", p,
68+
execErr := execution.ExecuteExt(ctx, "deployment.execute", p,
4569
deploy.MaybeRenderBlock(env, cluster, outputProgress),
4670
ExecuteOpts(),
4771
execution.FromContext(env),
4872
runtime.InjectCluster(cluster))
73+
observeError(ctx, execErr)
74+
return execErr
4975
}
5076

5177
return tasks.Action("orchestrator.deploy").Scope(schema.PackageNames(plan.FocusServer...)...).

orchestration/slack.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2022 Namespace Labs Inc; All rights reserved.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
5+
package orchestration
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"strings"
11+
"time"
12+
13+
"github.com/slack-go/slack"
14+
"k8s.io/utils/strings/slices"
15+
"namespacelabs.dev/foundation/schema"
16+
)
17+
18+
func renderSlackMessage(plan *schema.DeployPlan, start, end time.Time, err error) []slack.Block {
19+
var blocks []slack.Block
20+
blocks = append(blocks, slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, timeEmoji(end, err)+" "+deployLabel(end), true, false)))
21+
blocks = append(blocks, slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType,
22+
fmt.Sprintf("%s *%s*%s with:",
23+
workingLabel(end, err),
24+
plan.GetEnvironment().GetName(),
25+
maybeFrom()), false, false), nil, nil))
26+
blocks = append(blocks, slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType,
27+
strings.Join(servers(plan), "\n"), false, false), nil, nil))
28+
if !end.IsZero() {
29+
blocks = append(blocks, slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType,
30+
maybeTook(start, end), false, false), nil, nil))
31+
}
32+
if err != nil {
33+
blocks = append(blocks, slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType,
34+
fmt.Sprintf("*Error* %v", err), false, false), nil, nil))
35+
}
36+
return blocks
37+
}
38+
39+
func deployLabel(end time.Time) string {
40+
if end.IsZero() {
41+
return "Deploying"
42+
}
43+
44+
return "Deployed"
45+
}
46+
47+
func workingLabel(end time.Time, err error) string {
48+
if end.IsZero() {
49+
return "Updating"
50+
}
51+
52+
if err != nil {
53+
return "Failed to update"
54+
}
55+
56+
return "Updated"
57+
}
58+
59+
func servers(plan *schema.DeployPlan) []string {
60+
var fo []string
61+
62+
for _, ent := range plan.Stack.Entry {
63+
srv := ent.GetPackageName().String()
64+
if slices.Contains(plan.FocusServer, srv) {
65+
srv = fmt.Sprintf("*%s*", srv)
66+
}
67+
fo = append(fo, " · "+srv)
68+
}
69+
70+
return fo
71+
}
72+
73+
func timeEmoji(end time.Time, err error) string {
74+
if end.IsZero() {
75+
return ":hourglass_flowing_sand:"
76+
}
77+
78+
if err != nil {
79+
return ":boom:"
80+
}
81+
82+
return ":white_check_mark:"
83+
}
84+
85+
func maybeTook(start, end time.Time) string {
86+
if end.IsZero() {
87+
return ""
88+
}
89+
90+
return fmt.Sprintf("took %v", end.Sub(start))
91+
}
92+
93+
func maybeFrom() string {
94+
if bkUrl := os.Getenv("BUILDKITE_BUILD_URL"); bkUrl != "" {
95+
name := os.Getenv("BUILDKITE_PIPELINE_NAME")
96+
if name == "" {
97+
name = "Buildkite"
98+
} else {
99+
if number := os.Getenv("BUILDKITE_BUILD_NUMBER"); number != "" {
100+
name += " #" + number
101+
}
102+
}
103+
104+
if jobId := os.Getenv("BUILDKITE_JOB_ID"); jobId != "" {
105+
bkUrl += "#" + jobId
106+
}
107+
108+
return fmt.Sprintf(" from <%s|%s>", bkUrl, name)
109+
}
110+
111+
return ""
112+
}

0 commit comments

Comments
 (0)