Skip to content
Draft
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
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,13 @@ workflow-launch openshift-e2e-gcp 4.19 "BASELINE_CAPABILITY_SET=None","ADDITIONA
2. Add handler in appropriate event/interaction handler
3. Update help text in bot's supported commands

### Testing
### Testing and Verification
- Unit tests: `*_test.go` files (using Ginkgo/Gomega)
- Test command: `go test ./...`
- Integration tests in `pkg/manager/manager_test.go` and `pkg/manager/prow_test.go`
- **Before committing**: Run `make verify lint test all` to verify code is ready for commit
- This runs verification checks, linting, tests, and builds all targets
- Ensures code meets project quality standards

## Configuration

Expand Down
17 changes: 3 additions & 14 deletions pkg/slack/modals/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
localslack "github.com/openshift/ci-chat-bot/pkg/slack"
"github.com/openshift/ci-chat-bot/pkg/slack/interactions"
"github.com/openshift/ci-chat-bot/pkg/slack/modals"
"github.com/openshift/ci-chat-bot/pkg/slack/modals/common"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)
Expand All @@ -18,6 +19,7 @@ func Register(client *slack.Client, jobmanager manager.JobManager) *modals.FlowW
})
}

// process has custom kubeconfig handling logic
func process(updater *slack.Client, jobManager manager.JobManager) interactions.Handler {
return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) {
go func() {
Expand All @@ -28,20 +30,7 @@ func process(updater *slack.Client, jobManager manager.JobManager) interactions.
}
msg, kubeconfig := localslack.NotifyJob(updater, job, false)
submission := modals.SubmissionView(title, msg)
// add kubeconfig block if exists
if kubeconfig != "" {
submission.Blocks.BlockSet = append(submission.Blocks.BlockSet,
slack.NewDividerBlock(),
slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, "KubeConfig File (to download the kubeconfig as a file, type `auth` in the Messages tab):", true, false)),
slack.NewRichTextBlock("kubeconfig", &slack.RichTextPreformatted{
RichTextSection: slack.RichTextSection{
Type: slack.RTEPreformatted,
Elements: []slack.RichTextSectionElement{
slack.NewRichTextSectionTextElement(kubeconfig, &slack.RichTextSectionTextStyle{Code: false}),
},
},
}))
}
common.AppendKubeconfigBlock(&submission, kubeconfig, "KubeConfig File (to download the kubeconfig as a file, type `auth` in the Messages tab):")
modals.OverwriteView(updater, submission, callback, logger)
}()
return modals.SubmitPrepare(title, identifier, logger)
Expand Down
23 changes: 6 additions & 17 deletions pkg/slack/modals/auth/views.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
package auth

import (
"github.com/openshift/ci-chat-bot/pkg/slack/modals"
"github.com/openshift/ci-chat-bot/pkg/slack/modals/common"
slackClient "github.com/slack-go/slack"
)

func View() slackClient.ModalViewRequest {
return slackClient.ModalViewRequest{
Type: slackClient.VTModal,
PrivateMetadata: modals.CallbackDataToMetadata(modals.CallbackData{}, identifier),
Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: title},
Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"},
Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Submit"},
Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{
&slackClient.SectionBlock{
Type: slackClient.MBTSection,
Text: &slackClient.TextBlockObject{
Type: slackClient.MarkdownType,
Text: "Click submit to retrieve the credentials for your cluster",
},
},
}},
}
return common.BuildSimpleView(
identifier,
title,
"Click submit to retrieve the credentials for your cluster",
)
}
28 changes: 28 additions & 0 deletions pkg/slack/modals/common/releases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package common

import (
"encoding/json"
"fmt"
"net/http"

"k8s.io/klog"
)

// FetchReleases retrieves the accepted release streams from the OpenShift release controller
func FetchReleases(client *http.Client, architecture string) (map[string][]string, error) {
url := fmt.Sprintf("https://%s.ocp.releases.ci.openshift.org/api/v1/releasestreams/accepted", architecture)
acceptedReleases := make(map[string][]string, 0)
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
klog.Errorf("Failed to close response for FetchReleases: %v", closeErr)
}
}()
if err := json.NewDecoder(resp.Body).Decode(&acceptedReleases); err != nil {
return nil, err
}
return acceptedReleases, nil
}
145 changes: 145 additions & 0 deletions pkg/slack/modals/common/simple_modals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package common

import (
"fmt"
"strings"

"github.com/openshift/ci-chat-bot/pkg/manager"
"github.com/openshift/ci-chat-bot/pkg/slack/interactions"
"github.com/openshift/ci-chat-bot/pkg/slack/modals"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)

// SimpleModalConfig holds configuration for simple modal registration
type SimpleModalConfig struct {
Identifier string
Title string
ViewFunc func() slack.ModalViewRequest
}

// RegisterSimpleModal creates a standard modal registration with view submission handler
func RegisterSimpleModal(
config SimpleModalConfig,
processFunc func(*slack.Client, manager.JobManager) interactions.Handler,
) func(*slack.Client, manager.JobManager) *modals.FlowWithViewAndFollowUps {
return func(client *slack.Client, jobmanager manager.JobManager) *modals.FlowWithViewAndFollowUps {
return modals.ForView(modals.Identifier(config.Identifier), config.ViewFunc()).WithFollowUps(map[slack.InteractionType]interactions.Handler{
slack.InteractionTypeViewSubmission: processFunc(client, jobmanager),
})
}
}

// ProcessHandlerFunc is a function that performs the modal's main action
type ProcessHandlerFunc func(manager.JobManager, *slack.InteractionCallback) (string, error)

// MakeSimpleProcessHandler creates a standard process handler that:
// 1. Runs the action asynchronously
// 2. Handles errors with ErrorView
// 3. Shows success with SubmissionView
// 4. Returns SubmitPrepare immediately
func MakeSimpleProcessHandler(
identifier string,
title string,
actionFunc ProcessHandlerFunc,
errorContext string,
) func(*slack.Client, manager.JobManager) interactions.Handler {
return func(updater *slack.Client, jobManager manager.JobManager) interactions.Handler {
return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) {
go func() {
msg, err := actionFunc(jobManager, callback)
if err != nil {
modals.OverwriteView(updater, modals.ErrorView(errorContext, err), callback, logger)
return
}
modals.OverwriteView(updater, modals.SubmissionView(title, msg), callback, logger)
}()
return modals.SubmitPrepare(title, identifier, logger)
})
}
}

// BuildSimpleView creates a simple modal view with a single section message
func BuildSimpleView(identifier, title, message string) slack.ModalViewRequest {
return slack.ModalViewRequest{
Type: slack.VTModal,
PrivateMetadata: modals.CallbackDataToMetadata(modals.CallbackData{}, identifier),
Title: &slack.TextBlockObject{Type: slack.PlainTextType, Text: title},
Close: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Cancel"},
Submit: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Submit"},
Blocks: slack.Blocks{BlockSet: []slack.Block{
&slack.SectionBlock{
Type: slack.MBTSection,
Text: &slack.TextBlockObject{
Type: slack.MarkdownType,
Text: message,
},
},
}},
}
}

// AppendKubeconfigBlock appends a kubeconfig display block to a modal view
func AppendKubeconfigBlock(view *slack.ModalViewRequest, kubeconfig, headerText string) {
if kubeconfig == "" {
return
}
view.Blocks.BlockSet = append(view.Blocks.BlockSet,
slack.NewDividerBlock(),
slack.NewHeaderBlock(slack.NewTextBlockObject(slack.PlainTextType, headerText, true, false)),
slack.NewRichTextBlock("kubeconfig", &slack.RichTextPreformatted{
RichTextSection: slack.RichTextSection{
Type: slack.RTEPreformatted,
Elements: []slack.RichTextSectionElement{
slack.NewRichTextSectionTextElement(kubeconfig, &slack.RichTextSectionTextStyle{Code: false}),
},
},
}))
}

// BuildListResultModal creates a modal view for displaying list results
func BuildListResultModal(title, beginning string, elements []string) slack.ModalViewRequest {
submission := slack.ModalViewRequest{
Type: slack.VTModal,
Title: &slack.TextBlockObject{Type: slack.PlainTextType, Text: title},
Close: &slack.TextBlockObject{Type: slack.PlainTextType, Text: "Close"},
Blocks: slack.Blocks{BlockSet: []slack.Block{
slack.NewRichTextBlock("beginning", slack.NewRichTextSection(slack.NewRichTextSectionTextElement(beginning, &slack.RichTextSectionTextStyle{}))),
}},
}
for _, element := range elements {
submission.Blocks.BlockSet = append(submission.Blocks.BlockSet, slack.NewSectionBlock(slack.NewTextBlockObject(slack.MarkdownType, element, false, false), nil, nil))
}
return submission
}

// MetadataBuilder helps construct context metadata strings
type MetadataBuilder struct {
parts []string
}

// NewMetadataBuilder creates a new metadata builder
func NewMetadataBuilder() *MetadataBuilder {
return &MetadataBuilder{parts: make([]string, 0)}
}

// Add adds a key-value pair to the metadata
func (mb *MetadataBuilder) Add(key, value string) *MetadataBuilder {
if value != "" {
mb.parts = append(mb.parts, key+": "+value)
}
return mb
}

// AddInterface adds a key-value pair where value is any interface
func (mb *MetadataBuilder) AddInterface(key string, value interface{}) *MetadataBuilder {
if value != nil && value != "" {
mb.parts = append(mb.parts, fmt.Sprintf("%s: %v", key, value))
}
return mb
}

// Build returns the formatted metadata string
func (mb *MetadataBuilder) Build() string {
return strings.Join(mb.parts, "; ")
}
Loading