diff --git a/CLAUDE.md b/CLAUDE.md index 33897ea7c..74db5fb9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/pkg/slack/modals/auth/auth.go b/pkg/slack/modals/auth/auth.go index 1c7a0da30..f7e6adc82 100644 --- a/pkg/slack/modals/auth/auth.go +++ b/pkg/slack/modals/auth/auth.go @@ -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" ) @@ -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() { @@ -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) diff --git a/pkg/slack/modals/auth/views.go b/pkg/slack/modals/auth/views.go index e1277e8f3..590a3867c 100644 --- a/pkg/slack/modals/auth/views.go +++ b/pkg/slack/modals/auth/views.go @@ -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", + ) } diff --git a/pkg/slack/modals/common/releases.go b/pkg/slack/modals/common/releases.go new file mode 100644 index 000000000..4b4b2db5d --- /dev/null +++ b/pkg/slack/modals/common/releases.go @@ -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 +} diff --git a/pkg/slack/modals/common/simple_modals.go b/pkg/slack/modals/common/simple_modals.go new file mode 100644 index 000000000..63a97dfa8 --- /dev/null +++ b/pkg/slack/modals/common/simple_modals.go @@ -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, "; ") +} diff --git a/pkg/slack/modals/common/step_handlers.go b/pkg/slack/modals/common/step_handlers.go new file mode 100644 index 000000000..73d58b509 --- /dev/null +++ b/pkg/slack/modals/common/step_handlers.go @@ -0,0 +1,368 @@ +package common + +import ( + "fmt" + "net/http" + "strings" + "sync" + + "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" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog" +) + +// ParseModeSelections converts modal selection keys to mode set +func ParseModeSelections(selections []string) sets.Set[string] { + mode := sets.New[string]() + for _, selection := range selections { + switch selection { + case modals.LaunchModePRKey: + mode.Insert(modals.LaunchModePR) + case modals.LaunchModeVersionKey: + mode.Insert(modals.LaunchModeVersion) + } + } + return mode +} + +// HasPRMode checks if PR mode is enabled in the mode selections +func HasPRMode(mode []string) bool { + for _, key := range mode { + if strings.TrimSpace(key) == modals.LaunchModePRKey { + return true + } + } + return false +} + +// ValidatePRInput validates PR input by resolving each PR concurrently +func ValidatePRInput(submissionData modals.CallbackData, jobmanager manager.JobManager) []byte { + prs, ok := submissionData.Input[modals.LaunchFromPR] + if !ok { + return nil + } + + var wg sync.WaitGroup + errCh := make(chan error) + + prSlice := strings.Split(prs, ",") + for _, pr := range prSlice { + wg.Add(1) + go func(pr string) { + defer wg.Done() + prParts, err := jobmanager.ResolveAsPullRequest(pr) + if prParts == nil { + errCh <- fmt.Errorf("invalid PR(s)") + } + if err != nil { + errCh <- err + } + }(pr) + } + + go func() { + wg.Wait() + close(errCh) + }() + + errors := make(map[string]string) + var prErrors []string + + for err := range errCh { + prErrors = append(prErrors, err.Error()) + } + + if len(prErrors) == 0 { + return nil + } + + errors[modals.LaunchFromPR] = strings.Join(prErrors, "; ") + response, err := modals.ValidationError(errors) + if err != nil { + klog.Warningf("failed to build validation error: %v", err) + return nil + } + + return response +} + +// CheckVariables verifies that at most one variable is set +func CheckVariables(vars ...string) bool { + count := 0 + for _, v := range vars { + if v != "" { + count++ + } + } + return count <= 1 +} + +// ValidateFilterVersion validates that only one version source is selected +func ValidateFilterVersion(submissionData modals.CallbackData) []byte { + errs := make(map[string]string, 0) + nightlyOrCi := submissionData.Input[modals.LaunchFromLatestBuild] + if nightlyOrCi != "" { + errs[modals.LaunchFromLatestBuild] = "Select only one parameter!" + } + customBuild := submissionData.Input[modals.LaunchFromCustom] + if customBuild != "" { + errs[modals.LaunchFromCustom] = "Select only one parameter!" + } + selectedStream := submissionData.Input[modals.LaunchFromStream] + if selectedStream != "" { + errs[modals.LaunchFromStream] = "Select only one parameter!" + } + if !CheckVariables(nightlyOrCi, customBuild, selectedStream) { + response, err := modals.ValidationError(errs) + if err == nil { + return response + } + } + return nil +} + +// ViewFuncs groups all view builder functions for a flow +type ViewFuncs struct { + FilterVersionView func(*slack.InteractionCallback, manager.JobManager, modals.CallbackData, *http.Client, sets.Set[string], bool) slack.ModalViewRequest + PRInputView func(*slack.InteractionCallback, modals.CallbackData, string) slack.ModalViewRequest + ThirdStepView func(*slack.InteractionCallback, manager.JobManager, *http.Client, modals.CallbackData, string) slack.ModalViewRequest + SelectVersionView func(*slack.InteractionCallback, manager.JobManager, *http.Client, modals.CallbackData, string) slack.ModalViewRequest +} + +// MakeModeStepHandler creates a mode selection step handler +func MakeModeStepHandler( + identifier string, + modalTitle string, + filterVersionView func(*slack.InteractionCallback, manager.JobManager, modals.CallbackData, *http.Client, sets.Set[string], bool) slack.ModalViewRequest, + prInputView func(*slack.InteractionCallback, modals.CallbackData, string) slack.ModalViewRequest, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + submissionData := modals.MergeCallbackData(callback) + mode := ParseModeSelections(submissionData.MultipleSelection[modals.LaunchMode]) + go func() { + if mode.Has(modals.LaunchModeVersion) { + modals.OverwriteView(updater, filterVersionView(callback, jobmanager, submissionData, httpclient, mode, false), callback, logger) + } else { + modals.OverwriteView(updater, prInputView(callback, submissionData, identifier), callback, logger) + } + }() + return modals.SubmitPrepare(modalTitle, identifier, logger) + }) + } +} + +// MakePRInputHandler creates a PR input step handler +func MakePRInputHandler( + identifier string, + modalTitle string, + thirdStepView func(*slack.InteractionCallback, manager.JobManager, *http.Client, modals.CallbackData, string) slack.ModalViewRequest, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + submissionData := modals.MergeCallbackData(callback) + errorsResponse := ValidatePRInput(submissionData, jobmanager) + if errorsResponse != nil { + return errorsResponse, nil + } + go modals.OverwriteView(updater, thirdStepView(callback, jobmanager, httpclient, submissionData, identifier), callback, logger) + return modals.SubmitPrepare(modalTitle, identifier, logger) + }) + } +} + +// MakeFilterVersionHandler creates a filter version step handler +func MakeFilterVersionHandler( + identifier string, + modalTitle string, + returnIdentifier string, + views ViewFuncs, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + submissionData := modals.MergeCallbackData(callback) + errorResponse := ValidateFilterVersion(submissionData) + if errorResponse != nil { + return errorResponse, nil + } + nightlyOrCi := submissionData.Input[modals.LaunchFromLatestBuild] + customBuild := submissionData.Input[modals.LaunchFromCustom] + stream := submissionData.Input[modals.LaunchFromStream] + mode := submissionData.MultipleSelection[modals.LaunchMode] + hasPR := HasPRMode(mode) + go func() { + if (nightlyOrCi == "") && customBuild == "" && !hasPR && stream == "" { + modals.OverwriteView(updater, views.FilterVersionView(callback, jobmanager, submissionData, httpclient, sets.New(mode...), true), callback, logger) + } else if (nightlyOrCi != "" || customBuild != "") && hasPR { + modals.OverwriteView(updater, views.PRInputView(callback, submissionData, identifier), callback, logger) + } else if (nightlyOrCi != "" || customBuild != "") && !hasPR { + modals.OverwriteView(updater, views.ThirdStepView(callback, jobmanager, httpclient, submissionData, identifier), callback, logger) + } else { + modals.OverwriteView(updater, views.SelectVersionView(callback, jobmanager, httpclient, submissionData, identifier), callback, logger) + } + }() + return modals.SubmitPrepare(modalTitle, returnIdentifier, logger) + }) + } +} + +// MakeSelectVersionHandler creates a select version step handler +func MakeSelectVersionHandler( + identifier string, + modalTitle string, + prInputView func(*slack.InteractionCallback, modals.CallbackData, string) slack.ModalViewRequest, + thirdStepView func(*slack.InteractionCallback, manager.JobManager, *http.Client, modals.CallbackData, string) slack.ModalViewRequest, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + submissionData := modals.MergeCallbackData(callback) + mode := submissionData.MultipleSelection[modals.LaunchMode] + hasPR := HasPRMode(mode) + go func() { + if hasPR { + modals.OverwriteView(updater, prInputView(callback, submissionData, identifier), callback, logger) + } else { + modals.OverwriteView(updater, thirdStepView(callback, jobmanager, httpclient, submissionData, identifier), callback, logger) + } + }() + return modals.SubmitPrepare(modalTitle, identifier, logger) + }) + } +} + +// MakeSelectMinorMajorHandler creates a select minor/major version step handler +func MakeSelectMinorMajorHandler( + identifier string, + modalTitle string, + selectVersionView func(*slack.InteractionCallback, manager.JobManager, *http.Client, modals.CallbackData, string) slack.ModalViewRequest, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + submissionData := modals.MergeCallbackData(callback) + go modals.OverwriteView(updater, selectVersionView(callback, jobmanager, httpclient, submissionData, identifier), callback, logger) + return modals.SubmitPrepare(modalTitle, identifier, logger) + }) + } +} + +// FirstStepConfig holds configuration for the first step handler +type FirstStepConfig struct { + DefaultPlatform string + DefaultArchitecture string + // NeedsArchitecture indicates if architecture selection is required + NeedsArchitecture bool +} + +// MakeFirstStepHandler creates a first step handler with optional platform/architecture defaults +func MakeFirstStepHandler( + identifier string, + modalTitle string, + selectModeView func(*slack.InteractionCallback, manager.JobManager, modals.CallbackData) slack.ModalViewRequest, + config FirstStepConfig, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + go func() { + callbackData := modals.CallbackData{ + Input: modals.CallBackInputAll(callback), + } + + // Apply platform defaults if configured + if config.NeedsArchitecture { + if callbackData.Input[modals.LaunchPlatform] == "" { + callbackData.Input[modals.LaunchPlatform] = config.DefaultPlatform + } + if callbackData.Input[modals.LaunchArchitecture] == "" { + // Handle multi-arch for hypershift-hosted + if callbackData.Input[modals.LaunchPlatform] == "hypershift-hosted" { + callbackData.Input[modals.LaunchArchitecture] = "multi" + } else { + callbackData.Input[modals.LaunchArchitecture] = config.DefaultArchitecture + } + } + } + + modals.OverwriteView(updater, selectModeView(callback, jobmanager, callbackData), callback, logger) + }() + return modals.SubmitPrepare(modalTitle, identifier, logger) + }) + } +} + +// BackNavigationViews holds all view functions needed for back navigation +type BackNavigationViews struct { + FirstStepViewWithData func(modals.CallbackData) slack.ModalViewRequest + SelectModeView func(*slack.InteractionCallback, manager.JobManager, modals.CallbackData) slack.ModalViewRequest + FilterVersionView func(*slack.InteractionCallback, manager.JobManager, modals.CallbackData, *http.Client, sets.Set[string], bool) slack.ModalViewRequest + SelectMinorMajor func(*slack.InteractionCallback, *http.Client, modals.CallbackData, string) slack.ModalViewRequest + SelectVersionView func(*slack.InteractionCallback, manager.JobManager, *http.Client, modals.CallbackData, string) slack.ModalViewRequest + PRInputView func(*slack.InteractionCallback, modals.CallbackData, string) slack.ModalViewRequest +} + +// BackNavigationIdentifiers holds all step identifiers for a flow +type BackNavigationIdentifiers struct { + InitialView string + SelectModeView string + FilterVersionView string + SelectMinorMajor string + SelectVersion string + PRInputView string + ThirdStep string +} + +// MakeBackNavigationHandler creates a back button handler for a modal flow +func MakeBackNavigationHandler( + actionID string, + views BackNavigationViews, + identifiers BackNavigationIdentifiers, +) func(modals.ViewUpdater, manager.JobManager, *http.Client) interactions.Handler { + return func(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { + return interactions.HandlerFunc(actionID, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { + // Check if this is a back button action + if len(callback.ActionCallback.BlockActions) == 0 { + return nil, nil + } + action := callback.ActionCallback.BlockActions[0] + if action.ActionID != actionID { + return nil, nil + } + + // Get the callback data which contains the previous step + data := modals.MergeCallbackData(callback) + previousStep := data.PreviousStep + + logger.Infof("Back button pressed, navigating to previous step: %s", previousStep) + + var previousView slack.ModalViewRequest + switch previousStep { + case identifiers.InitialView: + previousView = views.FirstStepViewWithData(data) + case identifiers.SelectModeView: + previousView = views.SelectModeView(callback, jobmanager, data) + case identifiers.FilterVersionView: + mode := sets.New(data.MultipleSelection[modals.LaunchMode]...) + previousView = views.FilterVersionView(callback, jobmanager, data, httpclient, mode, false) + case identifiers.SelectMinorMajor: + previousView = views.SelectMinorMajor(callback, httpclient, data, identifiers.FilterVersionView) + case identifiers.SelectVersion: + previousView = views.SelectVersionView(callback, jobmanager, httpclient, data, identifiers.FilterVersionView) + case identifiers.PRInputView: + prPreviousStep := identifiers.SelectModeView + if data.Input[modals.LaunchVersion] != "" || data.Input[modals.LaunchFromLatestBuild] != "" || data.Input[modals.LaunchFromCustom] != "" { + prPreviousStep = identifiers.FilterVersionView + } + previousView = views.PRInputView(callback, data, prPreviousStep) + default: + logger.Warnf("Unknown previous step: %s, defaulting to first step", previousStep) + previousView = views.FirstStepViewWithData(data) + } + + modals.OverwriteView(updater, previousView, callback, logger) + return nil, nil + }) + } +} diff --git a/pkg/slack/modals/common/version_views.go b/pkg/slack/modals/common/version_views.go new file mode 100644 index 000000000..f4b8462fd --- /dev/null +++ b/pkg/slack/modals/common/version_views.go @@ -0,0 +1,582 @@ +package common + +import ( + "fmt" + "net/http" + "slices" + "sort" + "strings" + + "github.com/openshift/ci-chat-bot/pkg/manager" + "github.com/openshift/ci-chat-bot/pkg/slack/modals" + slackClient "github.com/slack-go/slack" + "golang.org/x/mod/semver" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog" +) + +const ( + // SlackUIMaxOptions is the maximum number of options that can be displayed in a Slack select menu + SlackUIMaxOptions = 99 +) + +// VersionViewConfig holds configuration for creating version selection views +type VersionViewConfig struct { + // Callback is the interaction callback from Slack + Callback *slackClient.InteractionCallback + // Data contains the accumulated user input from previous steps + Data modals.CallbackData + // JobManager is used to resolve versions + JobManager manager.JobManager + // HTTPClient is used to fetch releases + HTTPClient *http.Client + // Mode indicates the selected launch modes (PR, Version, or both) + Mode sets.Set[string] + // NoneSelected indicates if validation failed due to no selection + NoneSelected bool + // ModalIdentifier is the identifier for this modal step + ModalIdentifier string + // Title is the modal title + Title string + // PreviousStep is the identifier of the previous step (for back navigation) + PreviousStep string + // Architecture is the target architecture for release fetching + Architecture string + // ContextMetadata is additional context to display to the user + ContextMetadata string +} + +// BuildFilterVersionView creates a view for selecting versions from streams, builds, or custom specs +func BuildFilterVersionView(config VersionViewConfig) slackClient.ModalViewRequest { + if config.Callback == nil { + return slackClient.ModalViewRequest{} + } + + latestBuildOptions := []*slackClient.OptionBlockObject{} + _, nightly, _, err := config.JobManager.ResolveImageOrVersion("nightly", "", config.Architecture) + if err == nil { + latestBuildOptions = append(latestBuildOptions, &slackClient.OptionBlockObject{ + Value: "nightly", + Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: nightly}, + }) + } + _, ci, _, err := config.JobManager.ResolveImageOrVersion("ci", "", config.Architecture) + if err == nil { + latestBuildOptions = append(latestBuildOptions, &slackClient.OptionBlockObject{ + Value: "ci", + Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: ci}, + }) + } + + releases, err := FetchReleases(config.HTTPClient, config.Architecture) + if err != nil { + klog.Warningf("failed to fetch the data from release controller: %s", err) + return modals.ErrorView("retrieve valid releases from the release-controller", err) + } + + streams := filterStreams(releases, config.Data.Input[modals.LaunchPlatform]) + sort.Strings(streams) + streamsOptions := modals.BuildOptions(streams, nil) + + // Preserve previous selections + var streamInitial, latestBuildInitial *slackClient.OptionBlockObject + if stream, ok := config.Data.Input[modals.LaunchFromStream]; ok && stream != "" { + streamInitial = &slackClient.OptionBlockObject{ + Value: stream, + Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: stream}, + } + } + if build, ok := config.Data.Input[modals.LaunchFromLatestBuild]; ok && build != "" { + for _, opt := range latestBuildOptions { + if opt.Value == build { + latestBuildInitial = opt + break + } + } + } + customValue := config.Data.Input[modals.LaunchFromCustom] + + // Set the previous step for back navigation + config.Data = modals.SetPreviousStep(config.Data, config.PreviousStep) + + view := slackClient.ModalViewRequest{ + Type: slackClient.VTModal, + PrivateMetadata: modals.CallbackDataToMetadata(config.Data, config.ModalIdentifier), + Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: config.Title}, + Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, + Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, + Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ + modals.BackButtonBlock(), + &slackClient.HeaderBlock{ + Type: slackClient.MBTHeader, + Text: &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: "Version Specifications", + Emoji: false, + Verbatim: false, + }, + }, + &slackClient.SectionBlock{ + Type: slackClient.MBTSection, + Text: &slackClient.TextBlockObject{ + Type: slackClient.MarkdownType, + Text: "*Specify the _stream_ to get a list of versions to select from*", + }, + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchFromStream, + Optional: true, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Specify the Stream:"}, + Element: &slackClient.SelectBlockElement{ + Type: slackClient.OptTypeStatic, + Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, + Options: streamsOptions, + InitialOption: streamInitial, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider_section", + }, + &slackClient.SectionBlock{ + Type: slackClient.MBTSection, + Text: &slackClient.TextBlockObject{ + Type: slackClient.MarkdownType, + Text: "\n*Alternatively:*\n*Launch using the latest Nightly or CI build*", + }, + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchFromLatestBuild, + Optional: true, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "The latest build (nightly) or CI build:"}, + Element: &slackClient.SelectBlockElement{ + Type: slackClient.OptTypeStatic, + Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, + Options: latestBuildOptions, + InitialOption: latestBuildInitial, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider_2nd_section", + }, + &slackClient.SectionBlock{ + Type: slackClient.MBTSection, + Text: &slackClient.TextBlockObject{ + Type: slackClient.MarkdownType, + Text: "\n*Alternatively:*\n*Launch using a _Custom_ Pull Spec*", + }, + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchFromCustom, + Optional: true, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter a Custom Pull Spec:"}, + Element: &slackClient.PlainTextInputBlockElement{ + Type: slackClient.METPlainTextInput, + Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter a custom pull spec..."}, + InitialValue: customValue, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider", + }, + &slackClient.ContextBlock{ + Type: slackClient.MBTContext, + BlockID: modals.LaunchStepContext, + ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ + &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: config.ContextMetadata, + Emoji: false, + Verbatim: false, + }, + }}, + }, + }}, + } + + if config.NoneSelected { + view.Blocks.BlockSet = append([]slackClient.Block{ + slackClient.NewHeaderBlock(slackClient.NewTextBlockObject( + slackClient.PlainTextType, + ":warning: Error: At least one option must be selected :warning:", + true, + false, + )), + }, view.Blocks.BlockSet...) + } + + return view +} + +// BuildSelectVersionView creates a view for selecting a specific version from a stream +func BuildSelectVersionView(config VersionViewConfig) slackClient.ModalViewRequest { + if config.Callback == nil { + return slackClient.ModalViewRequest{} + } + + selectedStream := config.Data.Input[modals.LaunchFromStream] + selectedMajorMinor := config.Data.Input[modals.LaunchFromMajorMinor] + + releases, err := FetchReleases(config.HTTPClient, config.Architecture) + if err != nil { + klog.Warningf("failed to fetch the data from release controller: %s", err) + return modals.ErrorView("retrieve valid releases from the release-controller", err) + } + + var allTags []string + for stream, tags := range releases { + if stream == selectedStream { + for _, tag := range tags { + if strings.HasPrefix(tag, selectedMajorMinor) { + allTags = append(allTags, tag) + } + } + } + } + + // If too many results, redirect to major/minor selection + if len(allTags) > SlackUIMaxOptions { + return BuildSelectMinorMajorView(config) + } + + allTagsOptions := modals.BuildOptions(allTags, nil) + + // Set the previous step for back navigation + config.Data = modals.SetPreviousStep(config.Data, config.PreviousStep) + + return slackClient.ModalViewRequest{ + Type: slackClient.VTModal, + PrivateMetadata: modals.CallbackDataToMetadata(config.Data, config.ModalIdentifier), + Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: config.Title}, + Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, + Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, + Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ + modals.BackButtonBlock(), + &slackClient.HeaderBlock{ + Type: slackClient.MBTHeader, + Text: &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: "Select a Version", + Emoji: false, + Verbatim: false, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider", + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchVersion, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select a version:"}, + Element: &slackClient.SelectBlockElement{ + Type: slackClient.OptTypeStatic, + Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, + Options: allTagsOptions, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "context_divider", + }, + &slackClient.ContextBlock{ + Type: slackClient.MBTContext, + BlockID: modals.LaunchStepContext, + ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ + &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: config.ContextMetadata, + Emoji: false, + Verbatim: false, + }, + }}, + }, + }}, + } +} + +// BuildSelectMinorMajorView creates a view for selecting a major.minor version to filter results +func BuildSelectMinorMajorView(config VersionViewConfig) slackClient.ModalViewRequest { + if config.Callback == nil { + return slackClient.ModalViewRequest{} + } + + selectedStream := config.Data.Input[modals.LaunchFromStream] + platform := config.Data.Input[modals.LaunchPlatform] + + releases, err := FetchReleases(config.HTTPClient, config.Architecture) + if err != nil { + klog.Warningf("failed to fetch the data from release controller: %s", err) + return modals.ErrorView("retrieve valid releases from the release-controller", err) + } + + majorMinor := make(map[string]bool, 0) + for stream, tags := range releases { + if stream != selectedStream { + continue + } + if strings.HasPrefix(stream, modals.StableReleasesPrefix) { + for _, tag := range tags { + splitTag := strings.Split(tag, ".") + if len(splitTag) >= 2 { + majorMinor[fmt.Sprintf("%s.%s", splitTag[0], splitTag[1])] = true + } + } + } + } + + var majorMinorReleases []string + for key := range majorMinor { + if manager.HypershiftSupportedVersions.Versions.Has(key) || platform != "hypershift-hosted" { + majorMinorReleases = append(majorMinorReleases, key) + } + } + + // Use semver for proper version sorting + for index, version := range majorMinorReleases { + majorMinorReleases[index] = "v" + version + } + semver.Sort(majorMinorReleases) + for index, version := range majorMinorReleases { + majorMinorReleases[index] = strings.TrimPrefix(version, "v") + } + slices.Reverse(majorMinorReleases) + + majorMinorOptions := modals.BuildOptions(majorMinorReleases, nil) + + // Set the previous step for back navigation + config.Data = modals.SetPreviousStep(config.Data, config.PreviousStep) + + return slackClient.ModalViewRequest{ + Type: slackClient.VTModal, + PrivateMetadata: modals.CallbackDataToMetadata(config.Data, config.ModalIdentifier), + Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: config.Title}, + Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, + Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, + Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ + modals.BackButtonBlock(), + &slackClient.HeaderBlock{ + Type: slackClient.MBTHeader, + Text: &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: "There are too many results from the selected Stream. Select a Minor.Major as well", + Emoji: false, + Verbatim: false, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider", + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchFromMajorMinor, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Specify the Major.Minor:"}, + Element: &slackClient.SelectBlockElement{ + Type: slackClient.OptTypeStatic, + Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, + Options: majorMinorOptions, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "context_divider", + }, + &slackClient.ContextBlock{ + Type: slackClient.MBTContext, + BlockID: modals.LaunchStepContext, + ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ + &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: config.ContextMetadata, + Emoji: false, + Verbatim: false, + }, + }}, + }, + }}, + } +} + +// BuildPRInputView creates a view for entering PR numbers +func BuildPRInputView(config VersionViewConfig) slackClient.ModalViewRequest { + if config.Callback == nil { + return slackClient.ModalViewRequest{} + } + + // Set the previous step for back navigation + config.Data = modals.SetPreviousStep(config.Data, config.PreviousStep) + + return slackClient.ModalViewRequest{ + Type: slackClient.VTModal, + PrivateMetadata: modals.CallbackDataToMetadata(config.Data, config.ModalIdentifier), + Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: config.Title}, + Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, + Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, + Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ + modals.BackButtonBlock(), + &slackClient.HeaderBlock{ + Type: slackClient.MBTHeader, + Text: &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: "Enter A PR", + Emoji: false, + Verbatim: false, + }, + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchFromPR, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter one or more PRs, separated by comma:"}, + Element: &slackClient.PlainTextInputBlockElement{ + Type: slackClient.METPlainTextInput, + Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter one or more PRs..."}, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider", + }, + &slackClient.ContextBlock{ + Type: slackClient.MBTContext, + BlockID: modals.LaunchStepContext, + ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ + &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: config.ContextMetadata, + Emoji: false, + Verbatim: false, + }, + }}, + }, + }}, + } +} + +// filterStreams filters release streams based on platform support +func filterStreams(releases map[string][]string, platform string) []string { + var streams []string + for stream := range releases { + if platform == "hypershift-hosted" { + for _, v := range sets.List(manager.HypershiftSupportedVersions.Versions) { + if strings.HasPrefix(stream, v) || strings.Split(stream, "-")[1] == "dev" || strings.Split(stream, "-")[1] == "stable" { + streams = append(streams, stream) + break + } + } + } else { + streams = append(streams, stream) + } + } + return streams +} + +// SelectModeViewConfig holds configuration for building the select mode view +type SelectModeViewConfig struct { + Callback *slackClient.InteractionCallback + Data modals.CallbackData + ModalIdentifier string + Title string + PreviousStep string + ContextMetadata string +} + +// BuildSelectModeView creates a modal view for selecting launch mode (PR/Version) +func BuildSelectModeView(config SelectModeViewConfig) slackClient.ModalViewRequest { + if config.Callback == nil { + return slackClient.ModalViewRequest{} + } + + options := modals.BuildOptions([]string{modals.LaunchModePRKey, modals.LaunchModeVersionKey}, nil) + + // Build initial options from saved selections + var initialOptions []*slackClient.OptionBlockObject + if modes, ok := config.Data.MultipleSelection[modals.LaunchMode]; ok { + for _, mode := range modes { + initialOptions = append(initialOptions, &slackClient.OptionBlockObject{ + Value: mode, + Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: mode}, + }) + } + } + + // Set the previous step for back navigation + data := modals.SetPreviousStep(config.Data, config.PreviousStep) + + return slackClient.ModalViewRequest{ + Type: slackClient.VTModal, + PrivateMetadata: modals.CallbackDataToMetadata(data, config.ModalIdentifier), + Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: config.Title}, + Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, + Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, + Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ + modals.BackButtonBlock(), + &slackClient.HeaderBlock{ + Type: slackClient.MBTHeader, + Text: &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: "Select the launch mode", + Emoji: false, + Verbatim: false, + }, + }, + &slackClient.InputBlock{ + Type: slackClient.MBTInput, + BlockID: modals.LaunchMode, + Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch the Cluster using:"}, + Element: &slackClient.CheckboxGroupsBlockElement{ + Type: slackClient.METCheckboxGroups, + Options: options, + InitialOptions: initialOptions, + }, + }, + &slackClient.DividerBlock{ + Type: slackClient.MBTDivider, + BlockID: "divider", + }, + &slackClient.ContextBlock{ + Type: slackClient.MBTContext, + BlockID: modals.LaunchStepContext, + ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ + &slackClient.TextBlockObject{ + Type: slackClient.PlainTextType, + Text: config.ContextMetadata, + Emoji: false, + Verbatim: false, + }, + }}, + }, + }}, + } +} + +// BuildPRInputMetadata builds metadata string for PR input view +func BuildPRInputMetadata(data modals.CallbackData, baseMetadata string) string { + mode := data.MultipleSelection[modals.LaunchMode] + launchWithVersion := false + for _, key := range mode { + if strings.TrimSpace(key) == modals.LaunchModeVersionKey { + launchWithVersion = true + break + } + } + + if !launchWithVersion { + return baseMetadata + } + + version := data.Input[modals.LaunchVersion] + if version == "" { + version = data.Input[modals.LaunchFromLatestBuild] + } + if version == "" { + version = data.Input[modals.LaunchFromCustom] + } + + return fmt.Sprintf("%s;Version: %s", baseMetadata, version) +} diff --git a/pkg/slack/modals/done/done.go b/pkg/slack/modals/done/done.go index 80cd2a0f6..499420319 100644 --- a/pkg/slack/modals/done/done.go +++ b/pkg/slack/modals/done/done.go @@ -2,9 +2,8 @@ package done import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/slack-go/slack" ) @@ -12,21 +11,19 @@ const identifier = "done" const title = "Terminate a Cluster" func Register(client *slack.Client, jobmanager manager.JobManager) *modals.FlowWithViewAndFollowUps { - return modals.ForView(identifier, View()).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: process(client, jobmanager), - }) -} - -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() { - msg, err := jobManager.TerminateJobForUser(callback.User.ID) - if err != nil { - modals.OverwriteView(updater, modals.ErrorView("terminating the job", err), callback, logger) - return - } - modals.OverwriteView(updater, modals.SubmissionView(title, msg), callback, logger) - }() - return modals.SubmitPrepare(title, identifier, logger) - }) + return common.RegisterSimpleModal( + common.SimpleModalConfig{ + Identifier: identifier, + Title: title, + ViewFunc: View, + }, + common.MakeSimpleProcessHandler( + identifier, + title, + func(jobManager manager.JobManager, callback *slack.InteractionCallback) (string, error) { + return jobManager.TerminateJobForUser(callback.User.ID) + }, + "terminating job", + ), + )(client, jobmanager) } diff --git a/pkg/slack/modals/done/views.go b/pkg/slack/modals/done/views.go index 6105f976b..7e5e4f304 100644 --- a/pkg/slack/modals/done/views.go +++ b/pkg/slack/modals/done/views.go @@ -1,25 +1,14 @@ package done 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 terminate your running cluster", - }, - }, - }}, - } + return common.BuildSimpleView( + identifier, + title, + "Click submit to terminate your running cluster", + ) } diff --git a/pkg/slack/modals/launch/steps/back.go b/pkg/slack/modals/launch/steps/back.go index 0cee8f595..6e537e871 100644 --- a/pkg/slack/modals/launch/steps/back.go +++ b/pkg/slack/modals/launch/steps/back.go @@ -7,11 +7,11 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/apimachinery/pkg/util/sets" ) // RegisterBackButton registers the back button handler for both launch and MCE modal flows @@ -41,64 +41,51 @@ func handleBackButton(updater modals.ViewUpdater, jobmanager manager.JobManager, // Determine if this is an MCE modal based on the previous step identifier isMCE := strings.HasPrefix(previousStep, "mce_") - var previousView slack.ModalViewRequest + var handler interactions.Handler if isMCE { - previousView = handleMCEBackNavigation(callback, jobmanager, httpclient, data, previousStep, logger) + handler = common.MakeBackNavigationHandler( + modals.BackButtonActionID, + common.BackNavigationViews{ + FirstStepViewWithData: create.FirstStepViewWithData, + SelectModeView: create.SelectModeView, + FilterVersionView: create.FilterVersionView, + SelectMinorMajor: create.SelectMinorMajor, + SelectVersionView: create.SelectVersionView, + PRInputView: create.PRInputView, + }, + common.BackNavigationIdentifiers{ + InitialView: string(create.IdentifierInitialView), + SelectModeView: string(create.IdentifierSelectModeView), + FilterVersionView: string(create.IdentifierFilterVersionView), + SelectMinorMajor: string(create.IdentifierSelectMinorMajor), + SelectVersion: string(create.IdentifierSelectVersion), + PRInputView: string(create.IdentifierPRInputView), + ThirdStep: string(create.Identifier3rdStep), + }, + )(updater, jobmanager, httpclient) } else { - previousView = handleLaunchBackNavigation(callback, jobmanager, httpclient, data, previousStep, logger) + handler = common.MakeBackNavigationHandler( + modals.BackButtonActionID, + common.BackNavigationViews{ + FirstStepViewWithData: launch.FirstStepViewWithData, + SelectModeView: launch.SelectModeView, + FilterVersionView: launch.FilterVersionView, + SelectMinorMajor: launch.SelectMinorMajor, + SelectVersionView: launch.SelectVersionView, + PRInputView: launch.PRInputView, + }, + common.BackNavigationIdentifiers{ + InitialView: string(launch.IdentifierInitialView), + SelectModeView: string(launch.IdentifierRegisterLaunchMode), + FilterVersionView: string(launch.IdentifierFilterVersionView), + SelectMinorMajor: string(launch.IdentifierSelectMinorMajor), + SelectVersion: string(launch.IdentifierSelectVersion), + PRInputView: string(launch.IdentifierPRInputView), + ThirdStep: string(launch.Identifier3rdStep), + }, + )(updater, jobmanager, httpclient) } - modals.OverwriteView(updater, previousView, callback, logger) - return nil, nil + return handler.Handle(callback, logger) }) } - -func handleLaunchBackNavigation(callback *slack.InteractionCallback, jobmanager manager.JobManager, httpclient *http.Client, data modals.CallbackData, previousStep string, logger *logrus.Entry) slack.ModalViewRequest { - switch previousStep { - case string(launch.IdentifierInitialView): - return launch.FirstStepViewWithData(data) - case string(launch.IdentifierRegisterLaunchMode): - return launch.SelectModeView(callback, jobmanager, data) - case string(launch.IdentifierFilterVersionView): - mode := sets.New(data.MultipleSelection[modals.LaunchMode]...) - return launch.FilterVersionView(callback, jobmanager, data, httpclient, mode, false) - case string(launch.IdentifierSelectMinorMajor): - return launch.SelectMinorMajor(callback, httpclient, data, string(launch.IdentifierFilterVersionView)) - case string(launch.IdentifierSelectVersion): - return launch.SelectVersionView(callback, jobmanager, httpclient, data, string(launch.IdentifierFilterVersionView)) - case string(launch.IdentifierPRInputView): - prPreviousStep := string(launch.IdentifierRegisterLaunchMode) - if data.Input[modals.LaunchVersion] != "" || data.Input[modals.LaunchFromLatestBuild] != "" || data.Input[modals.LaunchFromCustom] != "" { - prPreviousStep = string(launch.IdentifierFilterVersionView) - } - return launch.PRInputView(callback, data, prPreviousStep) - default: - logger.Warnf("Unknown launch previous step: %s, defaulting to first step", previousStep) - return launch.FirstStepViewWithData(data) - } -} - -func handleMCEBackNavigation(callback *slack.InteractionCallback, jobmanager manager.JobManager, httpclient *http.Client, data modals.CallbackData, previousStep string, logger *logrus.Entry) slack.ModalViewRequest { - switch previousStep { - case string(create.IdentifierInitialView): - return create.FirstStepViewWithData(data) - case string(create.IdentifierSelectModeView): - return create.SelectModeView(callback, jobmanager, data) - case string(create.IdentifierFilterVersionView): - mode := sets.New(data.MultipleSelection[modals.LaunchMode]...) - return create.FilterVersionView(callback, jobmanager, data, httpclient, mode, false) - case string(create.IdentifierSelectMinorMajor): - return create.SelectMinorMajor(callback, httpclient, data, string(create.IdentifierFilterVersionView)) - case string(create.IdentifierSelectVersion): - return create.SelectVersionView(callback, jobmanager, httpclient, data, string(create.IdentifierFilterVersionView)) - case string(create.IdentifierPRInputView): - prPreviousStep := string(create.IdentifierSelectModeView) - if data.Input[modals.LaunchVersion] != "" || data.Input[modals.LaunchFromLatestBuild] != "" || data.Input[modals.LaunchFromCustom] != "" { - prPreviousStep = string(create.IdentifierFilterVersionView) - } - return create.PRInputView(callback, data, prPreviousStep) - default: - logger.Warnf("Unknown MCE previous step: %s, defaulting to first step", previousStep) - return create.FirstStepViewWithData(data) - } -} diff --git a/pkg/slack/modals/launch/steps/filterVersion.go b/pkg/slack/modals/launch/steps/filterVersion.go index e3c39eb61..a596068de 100644 --- a/pkg/slack/modals/launch/steps/filterVersion.go +++ b/pkg/slack/modals/launch/steps/filterVersion.go @@ -2,85 +2,27 @@ package steps import ( "net/http" - "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/apimachinery/pkg/util/sets" ) func RegisterFilterVersion(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(launch.IdentifierFilterVersionView, launch.FilterVersionView(nil, jobmanager, modals.CallbackData{}, httpclient, nil, false)).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextFilterVersion(client, jobmanager, httpclient), + slack.InteractionTypeViewSubmission: common.MakeFilterVersionHandler( + string(launch.IdentifierFilterVersionView), + launch.ModalTitle, + string(launch.IdentifierSelectVersion), + common.ViewFuncs{ + FilterVersionView: launch.FilterVersionView, + PRInputView: launch.PRInputView, + ThirdStepView: launch.ThirdStepView, + SelectVersionView: launch.SelectVersionView, + }, + )(client, jobmanager, httpclient), }) } - -func processNextFilterVersion(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(launch.IdentifierFilterVersionView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - submissionData := modals.MergeCallbackData(callback) - errorResponse := validateFilterVersion(submissionData) - if errorResponse != nil { - return errorResponse, nil - } - nightlyOrCi := submissionData.Input[modals.LaunchFromLatestBuild] - customBuild := submissionData.Input[modals.LaunchFromCustom] - stream := submissionData.Input[modals.LaunchFromStream] - mode := submissionData.MultipleSelection[modals.LaunchMode] - launchWithPr := false - for _, key := range mode { - if strings.TrimSpace(key) == modals.LaunchModePRKey { - launchWithPr = true - } - } - go func() { - if (nightlyOrCi == "") && customBuild == "" && !launchWithPr && stream == "" { - modals.OverwriteView(updater, launch.FilterVersionView(callback, jobmanager, submissionData, httpclient, sets.New(mode...), true), callback, logger) - } else if (nightlyOrCi != "" || customBuild != "") && launchWithPr { - modals.OverwriteView(updater, launch.PRInputView(callback, submissionData, string(launch.IdentifierFilterVersionView)), callback, logger) - } else if (nightlyOrCi != "" || customBuild != "") && !launchWithPr { - modals.OverwriteView(updater, launch.ThirdStepView(callback, jobmanager, httpclient, submissionData, string(launch.IdentifierFilterVersionView)), callback, logger) - } else { - modals.OverwriteView(updater, launch.SelectVersionView(callback, jobmanager, httpclient, submissionData, string(launch.IdentifierFilterVersionView)), callback, logger) - } - - }() - return modals.SubmitPrepare(launch.ModalTitle, string(launch.IdentifierSelectVersion), logger) - }) -} - -func checkVariables(vars ...string) bool { - count := 0 - for _, v := range vars { - if v != "" { - count++ - } - } - return count <= 1 -} - -func validateFilterVersion(submissionData modals.CallbackData) []byte { - errs := make(map[string]string, 0) - nightlyOrCi := submissionData.Input[modals.LaunchFromLatestBuild] - if nightlyOrCi != "" { - errs[modals.LaunchFromLatestBuild] = "Select only one parameter!" - } - customBuild := submissionData.Input[modals.LaunchFromCustom] - if customBuild != "" { - errs[modals.LaunchFromCustom] = "Select only one parameter!" - } - selectedStream := submissionData.Input[modals.LaunchFromStream] - if selectedStream != "" { - errs[modals.LaunchFromStream] = "Select only one parameter!" - } - if !checkVariables(nightlyOrCi, customBuild, selectedStream) { - response, err := modals.ValidationError(errs) - if err == nil { - return response - } - } - return nil -} diff --git a/pkg/slack/modals/launch/steps/intialStep.go b/pkg/slack/modals/launch/steps/intialStep.go index a6f2be8e9..b9213bda9 100644 --- a/pkg/slack/modals/launch/steps/intialStep.go +++ b/pkg/slack/modals/launch/steps/intialStep.go @@ -6,36 +6,22 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) func RegisterFirstStep(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(launch.IdentifierInitialView, launch.FirstStepView()).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextRegisterFirstStep(client, jobmanager, httpclient), - }) -} - -func processNextRegisterFirstStep(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(launch.IdentifierInitialView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - go func() { - callbackData := modals.CallbackData{ - Input: modals.CallBackInputAll(callback), - } - if callbackData.Input[modals.LaunchPlatform] == "" { - callbackData.Input[modals.LaunchPlatform] = launch.DefaultPlatform - } - if callbackData.Input[modals.LaunchArchitecture] == "" { - // TODO: handle more inteligently in the future or maybe default to multi - if callbackData.Input[modals.LaunchPlatform] == "hypershift-hosted" { - callbackData.Input[modals.LaunchArchitecture] = "multi" - } else { - callbackData.Input[modals.LaunchArchitecture] = launch.DefaultArchitecture - } - } - modals.OverwriteView(updater, launch.SelectModeView(callback, jobmanager, callbackData), callback, logger) - }() - return modals.SubmitPrepare(launch.ModalTitle, string(launch.IdentifierInitialView), logger) + slack.InteractionTypeViewSubmission: common.MakeFirstStepHandler( + string(launch.IdentifierInitialView), + launch.ModalTitle, + launch.SelectModeView, + common.FirstStepConfig{ + DefaultPlatform: launch.DefaultPlatform, + DefaultArchitecture: launch.DefaultArchitecture, + NeedsArchitecture: true, + }, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/launch/steps/launchMode.go b/pkg/slack/modals/launch/steps/launchMode.go index cd9a7e0fd..f5cf9ac59 100644 --- a/pkg/slack/modals/launch/steps/launchMode.go +++ b/pkg/slack/modals/launch/steps/launchMode.go @@ -6,38 +6,18 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/apimachinery/pkg/util/sets" ) func RegisterLaunchModeStep(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(launch.IdentifierRegisterLaunchMode, launch.SelectModeView(nil, jobmanager, modals.CallbackData{})).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextLaunchModeStep(client, jobmanager, httpclient), - }) -} - -func processNextLaunchModeStep(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(launch.IdentifierRegisterLaunchMode), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - submissionData := modals.MergeCallbackData(callback) - mode := sets.New[string]() - for _, selection := range submissionData.MultipleSelection[modals.LaunchMode] { - switch selection { - case modals.LaunchModePRKey: - mode.Insert(modals.LaunchModePR) - case modals.LaunchModeVersionKey: - mode.Insert(modals.LaunchModeVersion) - } - } - go func() { - if mode.Has(modals.LaunchModeVersion) { - modals.OverwriteView(updater, launch.FilterVersionView(callback, jobmanager, submissionData, httpclient, mode, false), callback, logger) - } else { - modals.OverwriteView(updater, launch.PRInputView(callback, submissionData, string(launch.IdentifierRegisterLaunchMode)), callback, logger) - } - - }() - return modals.SubmitPrepare(launch.ModalTitle, string(launch.IdentifierRegisterLaunchMode), logger) + slack.InteractionTypeViewSubmission: common.MakeModeStepHandler( + string(launch.IdentifierRegisterLaunchMode), + launch.ModalTitle, + launch.FilterVersionView, + launch.PRInputView, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/launch/steps/launchOptions.go b/pkg/slack/modals/launch/steps/launchOptions.go index d8bf09c39..fee8ee49a 100644 --- a/pkg/slack/modals/launch/steps/launchOptions.go +++ b/pkg/slack/modals/launch/steps/launchOptions.go @@ -65,9 +65,9 @@ func processLaunchOptionsStep(updater *slack.Client, jobmanager manager.JobManag go func() { msg, err := jobmanager.LaunchJobForUser(job) if err != nil { - modals.OverwriteView(updater, launch.SubmissionView(err.Error()), callback, logger) + modals.OverwriteView(updater, modals.SubmissionView(launch.ModalTitle, err.Error()), callback, logger) } else { - modals.OverwriteView(updater, launch.SubmissionView(msg), callback, logger) + modals.OverwriteView(updater, modals.SubmissionView(launch.ModalTitle, msg), callback, logger) } }() return modals.SubmitPrepare(launch.ModalTitle, string(launch.Identifier3rdStep), logger) diff --git a/pkg/slack/modals/launch/steps/prInput.go b/pkg/slack/modals/launch/steps/prInput.go index e3cfcc669..43ee13195 100644 --- a/pkg/slack/modals/launch/steps/prInput.go +++ b/pkg/slack/modals/launch/steps/prInput.go @@ -1,84 +1,22 @@ package steps import ( - "fmt" "net/http" - "strings" - "sync" "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/klog" ) func RegisterPRInput(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(launch.IdentifierPRInputView, launch.PRInputView(nil, modals.CallbackData{}, "")).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextPRInput(client, jobmanager, httpclient), + slack.InteractionTypeViewSubmission: common.MakePRInputHandler( + string(launch.IdentifierPRInputView), + launch.ModalTitle, + launch.ThirdStepView, + )(client, jobmanager, httpclient), }) } - -func processNextPRInput(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(launch.IdentifierPRInputView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - submissionData := modals.MergeCallbackData(callback) - errorsResponse := validatePRInputView(submissionData, jobmanager) - if errorsResponse != nil { - return errorsResponse, nil - } - go modals.OverwriteView(updater, launch.ThirdStepView(callback, jobmanager, httpclient, submissionData, string(launch.IdentifierPRInputView)), callback, logger) - return modals.SubmitPrepare(launch.ModalTitle, string(launch.IdentifierPRInputView), logger) - }) -} - -func validatePRInputView(submissionData modals.CallbackData, jobmanager manager.JobManager) []byte { - prs, ok := submissionData.Input[modals.LaunchFromPR] - if !ok { - return nil - } - - var wg sync.WaitGroup - errCh := make(chan error) - - prSlice := strings.Split(prs, ",") - for _, pr := range prSlice { - wg.Add(1) - go func(pr string) { - defer wg.Done() - prParts, err := jobmanager.ResolveAsPullRequest(pr) - if prParts == nil { - errCh <- fmt.Errorf("invalid PR(s)") - } - if err != nil { - errCh <- err - } - }(pr) - } - - go func() { - wg.Wait() - close(errCh) - }() - - errors := make(map[string]string) - var prErrors []string - - for err := range errCh { - prErrors = append(prErrors, err.Error()) - } - - if len(prErrors) == 0 { - return nil - } - - errors[modals.LaunchFromPR] = strings.Join(prErrors, "; ") - response, err := modals.ValidationError(errors) - if err != nil { - klog.Warningf("failed to build validation error: %v", err) - return nil - } - - return response -} diff --git a/pkg/slack/modals/launch/steps/selectMinorMajor.go b/pkg/slack/modals/launch/steps/selectMinorMajor.go index 42f421cde..de263f38f 100644 --- a/pkg/slack/modals/launch/steps/selectMinorMajor.go +++ b/pkg/slack/modals/launch/steps/selectMinorMajor.go @@ -6,21 +6,17 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) func RegisterSelectMinorMajor(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(launch.IdentifierSelectMinorMajor, launch.SelectMinorMajor(nil, httpclient, modals.CallbackData{}, "")).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextSelectMinorMajor(client, jobmanager, httpclient), - }) -} - -func processNextSelectMinorMajor(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(launch.IdentifierSelectMinorMajor), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - submissionData := modals.MergeCallbackData(callback) - go modals.OverwriteView(updater, launch.SelectVersionView(callback, jobmanager, httpclient, submissionData, string(launch.IdentifierSelectMinorMajor)), callback, logger) - return modals.SubmitPrepare(launch.ModalTitle, string(launch.IdentifierSelectMinorMajor), logger) + slack.InteractionTypeViewSubmission: common.MakeSelectMinorMajorHandler( + string(launch.IdentifierSelectMinorMajor), + launch.ModalTitle, + launch.SelectVersionView, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/launch/steps/selectVersion.go b/pkg/slack/modals/launch/steps/selectVersion.go index 8ee4a1baf..30c67dff7 100644 --- a/pkg/slack/modals/launch/steps/selectVersion.go +++ b/pkg/slack/modals/launch/steps/selectVersion.go @@ -2,39 +2,22 @@ package steps import ( "net/http" - "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/launch" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) func RegisterSelectVersion(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(launch.IdentifierSelectVersion, launch.SelectVersionView(nil, jobmanager, httpclient, modals.CallbackData{}, "")).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextSelectVersion(client, jobmanager, httpclient), - }) -} - -func processNextSelectVersion(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(launch.IdentifierSelectVersion), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - submissionData := modals.MergeCallbackData(callback) - mode := submissionData.MultipleSelection[modals.LaunchMode] - launchWithPR := false - for _, key := range mode { - if strings.TrimSpace(key) == modals.LaunchModePRKey { - launchWithPR = true - } - } - go func() { - if launchWithPR { - modals.OverwriteView(updater, launch.PRInputView(callback, submissionData, string(launch.IdentifierSelectVersion)), callback, logger) - } else { - modals.OverwriteView(updater, launch.ThirdStepView(callback, jobmanager, httpclient, submissionData, string(launch.IdentifierSelectVersion)), callback, logger) - } - }() - return modals.SubmitPrepare(launch.ModalTitle, string(launch.IdentifierSelectVersion), logger) + slack.InteractionTypeViewSubmission: common.MakeSelectVersionHandler( + string(launch.IdentifierSelectVersion), + launch.ModalTitle, + launch.PRInputView, + launch.ThirdStepView, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/launch/views.go b/pkg/slack/modals/launch/views.go index 6b345bc2e..2d2067225 100644 --- a/pkg/slack/modals/launch/views.go +++ b/pkg/slack/modals/launch/views.go @@ -1,39 +1,17 @@ package launch import ( - "encoding/json" "fmt" "net/http" - "slices" - "sort" "strings" "github.com/openshift/ci-chat-bot/pkg/manager" "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" - "golang.org/x/mod/semver" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/klog" ) -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 Resolve: %v", closeErr) - } - }() - if err := json.NewDecoder(resp.Body).Decode(&acceptedReleases); err != nil { - return nil, err - } - return acceptedReleases, nil -} - func FirstStepView() slackClient.ModalViewRequest { return FirstStepViewWithData(modals.CallbackData{}) } @@ -124,7 +102,12 @@ func ThirdStepView(callback *slackClient.InteractionCallback, jobmanager manager } } options := modals.BuildOptions(manager.SupportedParameters, blacklist) - context := fmt.Sprintf("Architecture: %s;Platform: %s;Version: %s;PR: %s", architecture, platform, version, prs) + context := common.NewMetadataBuilder(). + Add("Architecture", architecture). + Add("Platform", platform). + Add("Version", version). + Add("PR", prs). + Build() // Set the previous step for back navigation data = modals.SetPreviousStep(data, previousStep) return slackClient.ModalViewRequest{ @@ -166,27 +149,7 @@ func ThirdStepView(callback *slackClient.InteractionCallback, jobmanager manager } } -func SubmissionView(msg string) slackClient.ModalViewRequest { - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Close"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: msg, - }, - }, - }}, - } -} - func SelectModeView(callback *slackClient.InteractionCallback, jobmanager manager.JobManager, data modals.CallbackData) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } platform, ok := data.Input[modals.LaunchPlatform] if !ok { platform = DefaultPlatform @@ -195,491 +158,109 @@ func SelectModeView(callback *slackClient.InteractionCallback, jobmanager manage if !ok { architecture = DefaultArchitecture } - metadata := fmt.Sprintf("Architecture: %s; Platform: %s", architecture, platform) - options := modals.BuildOptions([]string{modals.LaunchModePRKey, modals.LaunchModeVersionKey}, nil) - - // Build initial options from saved selections - var initialOptions []*slackClient.OptionBlockObject - if modes, ok := data.MultipleSelection[modals.LaunchMode]; ok { - for _, mode := range modes { - initialOptions = append(initialOptions, &slackClient.OptionBlockObject{ - Value: mode, - Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: mode}, - }) - } - } + metadata := common.NewMetadataBuilder(). + Add("Architecture", architecture). + Add("Platform", platform). + Build() - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, string(IdentifierInitialView)) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierRegisterLaunchMode)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Select the launch mode", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchMode, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch the Cluster using:"}, - Element: &slackClient.CheckboxGroupsBlockElement{ - Type: slackClient.METCheckboxGroups, - Options: options, - InitialOptions: initialOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + return common.BuildSelectModeView(common.SelectModeViewConfig{ + Callback: callback, + Data: data, + ModalIdentifier: string(IdentifierRegisterLaunchMode), + Title: ModalTitle, + PreviousStep: string(IdentifierInitialView), + ContextMetadata: metadata, + }) } func FilterVersionView(callback *slackClient.InteractionCallback, jobmanager manager.JobManager, data modals.CallbackData, httpclient *http.Client, mode sets.Set[string], noneSelected bool) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } platform := data.Input[modals.LaunchPlatform] architecture := data.Input[modals.LaunchArchitecture] - latestBuildOptions := []*slackClient.OptionBlockObject{} - _, nightly, _, err := jobmanager.ResolveImageOrVersion("nightly", "", architecture) - if err == nil { - latestBuildOptions = append(latestBuildOptions, &slackClient.OptionBlockObject{Value: "nightly", Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: nightly}}) - } - _, ci, _, err := jobmanager.ResolveImageOrVersion("ci", "", architecture) - if err == nil { - latestBuildOptions = append(latestBuildOptions, &slackClient.OptionBlockObject{Value: "ci", Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: ci}}) - } - releases, err := FetchReleases(httpclient, architecture) - if err != nil { - klog.Warningf("failed to fetch the data from release controller: %s", err) - return modals.ErrorView("retrieve valid releases from the release-controller", err) - } - var streams []string - for stream := range releases { - if platform == "hypershift-hosted" { - for _, v := range sets.List(manager.HypershiftSupportedVersions.Versions) { - if strings.HasPrefix(stream, v) || strings.Split(stream, "-")[1] == "dev" || strings.Split(stream, "-")[1] == "stable" { - streams = append(streams, stream) - break - } - } - } else { - streams = append(streams, stream) - } - - } - - sort.Strings(streams) - streamsOptions := modals.BuildOptions(streams, nil) - metadata := fmt.Sprintf("Architecture: %s;Platform: %s;%s: %s", architecture, platform, modals.LaunchModeContext, strings.Join(sets.List(mode), ",")) - - // Preserve previous selections - var streamInitial, latestBuildInitial *slackClient.OptionBlockObject - if stream, ok := data.Input[modals.LaunchFromStream]; ok && stream != "" { - streamInitial = &slackClient.OptionBlockObject{ - Value: stream, - Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: stream}, - } - } - if build, ok := data.Input[modals.LaunchFromLatestBuild]; ok && build != "" { - // Find the display text for the build option - for _, opt := range latestBuildOptions { - if opt.Value == build { - latestBuildInitial = opt - break - } - } - } - customValue := data.Input[modals.LaunchFromCustom] + metadata := common.NewMetadataBuilder(). + Add("Architecture", architecture). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(sets.List(mode), ",")). + Build() - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, string(IdentifierRegisterLaunchMode)) - view := slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierFilterVersionView)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Version Specifications", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "*Specify the _stream_ to get a list of versions to select from*", - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromStream, - Optional: true, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Specify the Stream:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: streamsOptions, - InitialOption: streamInitial, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider_section", - }, - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "\n*Alternatively:*\n*Launch using the latest Nightly or CI build*", - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromLatestBuild, - Optional: true, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "The latest build (nightly) or CI build:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: latestBuildOptions, - InitialOption: latestBuildInitial, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider_2nd_section", - }, - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "\n*Alternatively:*\n*Launch using a _Custom_ Pull Spec*", - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromCustom, - Optional: true, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter a Custom Pull Spec:"}, - Element: &slackClient.PlainTextInputBlockElement{ - Type: slackClient.METPlainTextInput, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter a custom pull spec..."}, - InitialValue: customValue, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } - if noneSelected { - view.Blocks.BlockSet = append([]slackClient.Block{slackClient.NewHeaderBlock(slackClient.NewTextBlockObject(slackClient.PlainTextType, ":warning: Error: At least one option must be selected :warning:", true, false))}, view.Blocks.BlockSet...) - } - return view + return common.BuildFilterVersionView(common.VersionViewConfig{ + Callback: callback, + Data: data, + JobManager: jobmanager, + HTTPClient: httpclient, + Mode: mode, + NoneSelected: noneSelected, + ModalIdentifier: string(IdentifierFilterVersionView), + Title: ModalTitle, + PreviousStep: string(IdentifierRegisterLaunchMode), + Architecture: architecture, + ContextMetadata: metadata, + }) } func PRInputView(callback *slackClient.InteractionCallback, data modals.CallbackData, previousStep string) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } platform := data.Input[modals.LaunchPlatform] architecture := data.Input[modals.LaunchArchitecture] mode := data.MultipleSelection[modals.LaunchMode] - launchWithVersion := false - for _, key := range mode { - if strings.TrimSpace(key) == modals.LaunchModeVersionKey { - launchWithVersion = true - } - } - metadata := fmt.Sprintf("Architecture: %s; Platform: %s;%s: %s", architecture, platform, modals.LaunchModeContext, mode) - if launchWithVersion { - version := data.Input[modals.LaunchVersion] - if version == "" { - version = data.Input[modals.LaunchFromLatestBuild] - } - if version == "" { - version = data.Input[modals.LaunchFromCustom] - } - metadata = fmt.Sprintf("%s;Version: %s", metadata, version) - } - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, previousStep) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierPRInputView)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Enter A PR", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromPR, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter one or more PRs, separated by coma:"}, - Element: &slackClient.PlainTextInputBlockElement{ - Type: slackClient.METPlainTextInput, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter one or more PRs..."}, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + baseMetadata := common.NewMetadataBuilder(). + Add("Architecture", architecture). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(mode, ",")). + Build() + metadata := common.BuildPRInputMetadata(data, baseMetadata) + + return common.BuildPRInputView(common.VersionViewConfig{ + Callback: callback, + Data: data, + ModalIdentifier: string(IdentifierPRInputView), + Title: ModalTitle, + PreviousStep: previousStep, + ContextMetadata: metadata, + }) } func SelectVersionView(callback *slackClient.InteractionCallback, jobmanager manager.JobManager, httpclient *http.Client, data modals.CallbackData, previousStep string) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } - platform := data.Input[modals.LaunchPlatform] architecture := data.Input[modals.LaunchArchitecture] mode := data.MultipleSelection[modals.LaunchMode] - selectedStream := data.Input[modals.LaunchFromStream] - if selectedStream == "" { - selectedStream = data.Input[modals.LaunchFromStream] - } - selectedMajorMinor := data.Input[modals.LaunchFromMajorMinor] - metadata := fmt.Sprintf("Architecture: %s; Platform: %s; %s: %s", architecture, platform, modals.LaunchModeContext, mode) - releases, err := FetchReleases(httpclient, architecture) - if err != nil { - klog.Warningf("failed to fetch the data from release controller: %s", err) - return modals.ErrorView("retrieve valid releases from the release-controller", err) - } - var allTags []string - for stream, tags := range releases { - if stream == selectedStream { - for _, tag := range tags { - if strings.HasPrefix(tag, selectedMajorMinor) { - allTags = append(allTags, tag) - } - } + metadata := common.NewMetadataBuilder(). + Add("Architecture", architecture). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(mode, ",")). + Build() - } - } - if len(allTags) > 99 { - return SelectMinorMajor(callback, httpclient, data, previousStep) - } - //sort.Strings(allTags) - allTagsOptions := modals.BuildOptions(allTags, nil) - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, previousStep) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierSelectVersion)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Select a Version", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchVersion, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select a version:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: allTagsOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "context_divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + return common.BuildSelectVersionView(common.VersionViewConfig{ + Callback: callback, + Data: data, + JobManager: jobmanager, + HTTPClient: httpclient, + ModalIdentifier: string(IdentifierSelectVersion), + Title: ModalTitle, + PreviousStep: previousStep, + Architecture: architecture, + ContextMetadata: metadata, + }) } func SelectMinorMajor(callback *slackClient.InteractionCallback, httpclient *http.Client, data modals.CallbackData, previousStep string) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } - platform := data.Input[modals.LaunchPlatform] architecture := data.Input[modals.LaunchArchitecture] mode := data.MultipleSelection[modals.LaunchMode] selectedStream := data.Input[modals.LaunchFromStream] - metadata := fmt.Sprintf("Architecture: %s; Platform: %s; %s: %s; %s: %s", architecture, platform, modals.LaunchModeContext, mode, modals.LaunchFromStream, selectedStream) - releases, err := FetchReleases(httpclient, architecture) - if err != nil { - klog.Warningf("failed to fetch the data from release controller: %s", err) - return modals.ErrorView("retrieve valid releases from the release-controller", err) - } - - majorMinor := make(map[string]bool, 0) - for stream, tags := range releases { - if stream != selectedStream { - continue - } - if strings.HasPrefix(stream, modals.StableReleasesPrefix) { - for _, tag := range tags { - splitTag := strings.Split(tag, ".") - if len(splitTag) >= 2 { - majorMinor[fmt.Sprintf("%s.%s", splitTag[0], splitTag[1])] = true - } - } - - } - } - var majorMinorReleases []string - for key := range majorMinor { - if manager.HypershiftSupportedVersions.Versions.Has(key) || platform != "hypershift-hosted" { - majorMinorReleases = append(majorMinorReleases, key) - } + metadata := common.NewMetadataBuilder(). + Add("Architecture", architecture). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(mode, ",")). + Add(modals.LaunchFromStream, selectedStream). + Build() - } - // the x/mod/semver requires a `v` prefix for a version to be considered valid - for index, version := range majorMinorReleases { - majorMinorReleases[index] = "v" + version - } - semver.Sort(majorMinorReleases) - for index, version := range majorMinorReleases { - majorMinorReleases[index] = strings.TrimPrefix(version, "v") - } - slices.Reverse(majorMinorReleases) - majorMinorOptions := modals.BuildOptions(majorMinorReleases, nil) - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, previousStep) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierSelectMinorMajor)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "There are to many results from the selected Stream. Select a Minor.Major as well", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromMajorMinor, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Specify the Major.Minor:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: majorMinorOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "context_divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + return common.BuildSelectMinorMajorView(common.VersionViewConfig{ + Callback: callback, + Data: data, + HTTPClient: httpclient, + ModalIdentifier: string(IdentifierSelectMinorMajor), + Title: ModalTitle, + PreviousStep: previousStep, + Architecture: architecture, + ContextMetadata: metadata, + }) } diff --git a/pkg/slack/modals/list/list.go b/pkg/slack/modals/list/list.go index 5dc1d26cd..564b02821 100644 --- a/pkg/slack/modals/list/list.go +++ b/pkg/slack/modals/list/list.go @@ -4,6 +4,7 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) @@ -38,18 +39,7 @@ func process(updater *slack.Client, jobmanager manager.JobManager) interactions. } } _, beginning, elements := jobmanager.ListJobs(callback.User.ID, filters) - - 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)) - } + submission := common.BuildListResultModal(title, beginning, elements) modals.OverwriteView(updater, submission, callback, logger) }() return modals.SubmitPrepare(title, identifier, logger) diff --git a/pkg/slack/modals/mce/auth/auth.go b/pkg/slack/modals/mce/auth/auth.go index ce760e641..8ca5f4f24 100644 --- a/pkg/slack/modals/mce/auth/auth.go +++ b/pkg/slack/modals/mce/auth/auth.go @@ -7,6 +7,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" ) @@ -20,6 +21,7 @@ func Register(client *slack.Client, jobmanager manager.JobManager, httpclient *h }) } +// process has custom kubeconfig and cluster selection logic func process(updater *slack.Client, jobManager manager.JobManager, httpclient *http.Client) interactions.Handler { return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { go func() { @@ -39,20 +41,7 @@ func process(updater *slack.Client, jobManager manager.JobManager, httpclient *h msg, kubeconfig = localslack.NotifyMce(updater, managed[name], deployments[name], provisions[name], kubeconfigs[name], passwords[name], false, nil) } 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 `mce 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 `mce auth` in the Messages tab):") modals.OverwriteView(updater, submission, callback, logger) }() return modals.SubmitPrepare(title, identifier, logger) diff --git a/pkg/slack/modals/mce/auth/views.go b/pkg/slack/modals/mce/auth/views.go index 2942e8716..16e456f73 100644 --- a/pkg/slack/modals/mce/auth/views.go +++ b/pkg/slack/modals/mce/auth/views.go @@ -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: "MCE Authentication"}, - 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 MCE cluster", - }, - }, - }}, - } + return common.BuildSimpleView( + identifier, + title, + "Click submit to retrieve the credentials for your MCE cluster", + ) } diff --git a/pkg/slack/modals/mce/create/steps/createConfirm.go b/pkg/slack/modals/mce/create/steps/createConfirm.go index c840e5aa8..dbbf01c7a 100644 --- a/pkg/slack/modals/mce/create/steps/createConfirm.go +++ b/pkg/slack/modals/mce/create/steps/createConfirm.go @@ -11,7 +11,6 @@ import ( "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/klog" ) func RegisterCreateConfirmStep(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { @@ -22,7 +21,6 @@ func RegisterCreateConfirmStep(client *slack.Client, jobmanager manager.JobManag func processLaunchOptionsStep(updater *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { return interactions.HandlerFunc(string(create.Identifier3rdStep), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - klog.Infof("Private Metadata: %s", callback.View.PrivateMetadata) var createInputs []string data := modals.MergeCallbackData(callback) platform := data.Input[modals.LaunchPlatform] diff --git a/pkg/slack/modals/mce/create/steps/filterVersion.go b/pkg/slack/modals/mce/create/steps/filterVersion.go index a29d8902d..d32689f82 100644 --- a/pkg/slack/modals/mce/create/steps/filterVersion.go +++ b/pkg/slack/modals/mce/create/steps/filterVersion.go @@ -2,87 +2,27 @@ package steps import ( "net/http" - "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/klog" ) func RegisterFilterVersion(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(create.IdentifierFilterVersionView, create.FilterVersionView(nil, jobmanager, modals.CallbackData{}, httpclient, nil, false)).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextFilterVersion(client, jobmanager, httpclient), + slack.InteractionTypeViewSubmission: common.MakeFilterVersionHandler( + string(create.IdentifierFilterVersionView), + create.ModalTitle, + string(create.IdentifierFilterVersionView), + common.ViewFuncs{ + FilterVersionView: create.FilterVersionView, + PRInputView: create.PRInputView, + ThirdStepView: create.ThirdStepView, + SelectVersionView: create.SelectVersionView, + }, + )(client, jobmanager, httpclient), }) } - -func processNextFilterVersion(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(create.IdentifierFilterVersionView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - klog.Infof("Private Metadata: %s", callback.View.PrivateMetadata) - submissionData := modals.MergeCallbackData(callback) - errorResponse := validateFilterVersion(submissionData) - if errorResponse != nil { - return errorResponse, nil - } - nightlyOrCi := submissionData.Input[modals.LaunchFromLatestBuild] - customBuild := submissionData.Input[modals.LaunchFromCustom] - stream := submissionData.Input[modals.LaunchFromStream] - mode := submissionData.MultipleSelection[modals.LaunchMode] - createWithPr := false - for _, key := range mode { - if strings.TrimSpace(key) == modals.LaunchModePRKey { - createWithPr = true - } - } - go func() { - if (nightlyOrCi == "") && customBuild == "" && !createWithPr && stream == "" { - modals.OverwriteView(updater, create.FilterVersionView(callback, jobmanager, submissionData, httpclient, sets.New(mode...), true), callback, logger) - } else if (nightlyOrCi != "" || customBuild != "") && createWithPr { - modals.OverwriteView(updater, create.PRInputView(callback, submissionData, string(create.IdentifierFilterVersionView)), callback, logger) - } else if (nightlyOrCi != "" || customBuild != "") && !createWithPr { - modals.OverwriteView(updater, create.ThirdStepView(callback, jobmanager, httpclient, submissionData, string(create.IdentifierFilterVersionView)), callback, logger) - } else { - modals.OverwriteView(updater, create.SelectVersionView(callback, jobmanager, httpclient, submissionData, string(create.IdentifierFilterVersionView)), callback, logger) - } - - }() - return modals.SubmitPrepare(create.ModalTitle, string(create.IdentifierFilterVersionView), logger) - }) -} - -func checkVariables(vars ...string) bool { - count := 0 - for _, v := range vars { - if v != "" { - count++ - } - } - return count <= 1 -} - -func validateFilterVersion(submissionData modals.CallbackData) []byte { - errs := make(map[string]string, 0) - nightlyOrCi := submissionData.Input[modals.LaunchFromLatestBuild] - if nightlyOrCi != "" { - errs[modals.LaunchFromLatestBuild] = "Select only one parameter!" - } - customBuild := submissionData.Input[modals.LaunchFromCustom] - if customBuild != "" { - errs[modals.LaunchFromCustom] = "Select only one parameter!" - } - selectedStream := submissionData.Input[modals.LaunchFromStream] - if selectedStream != "" { - errs[modals.LaunchFromStream] = "Select only one parameter!" - } - if !checkVariables(nightlyOrCi, customBuild, selectedStream) { - response, err := modals.ValidationError(errs) - if err == nil { - return response - } - } - return nil -} diff --git a/pkg/slack/modals/mce/create/steps/firstStep.go b/pkg/slack/modals/mce/create/steps/firstStep.go index 03bc5bc51..228ee9e97 100644 --- a/pkg/slack/modals/mce/create/steps/firstStep.go +++ b/pkg/slack/modals/mce/create/steps/firstStep.go @@ -6,25 +6,20 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) func RegisterFirstStep(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(create.IdentifierInitialView, create.FirstStepView()).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextRegisterFirstStep(client, jobmanager, httpclient), - }) -} - -func processNextRegisterFirstStep(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(create.IdentifierInitialView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - go func() { - callbackData := modals.CallbackData{ - Input: modals.CallBackInputAll(callback), - } - modals.OverwriteView(updater, create.SelectModeView(callback, jobmanager, callbackData), callback, logger) - }() - return modals.SubmitPrepare(create.ModalTitle, string(create.IdentifierInitialView), logger) + slack.InteractionTypeViewSubmission: common.MakeFirstStepHandler( + string(create.IdentifierInitialView), + create.ModalTitle, + create.SelectModeView, + common.FirstStepConfig{ + NeedsArchitecture: false, + }, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/mce/create/steps/mode.go b/pkg/slack/modals/mce/create/steps/mode.go index 3a65ffccd..d58c23bfb 100644 --- a/pkg/slack/modals/mce/create/steps/mode.go +++ b/pkg/slack/modals/mce/create/steps/mode.go @@ -6,40 +6,18 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/klog" ) func RegisterLaunchModeStep(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(create.IdentifierSelectModeView, create.SelectModeView(nil, jobmanager, modals.CallbackData{})).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextLaunchModeStep(client, jobmanager, httpclient), - }) -} - -func processNextLaunchModeStep(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(create.IdentifierSelectModeView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - klog.Infof("Private Metadata: %s", callback.View.PrivateMetadata) - submissionData := modals.MergeCallbackData(callback) - mode := sets.New[string]() - for _, selection := range submissionData.MultipleSelection[modals.LaunchMode] { - switch selection { - case modals.LaunchModePRKey: - mode.Insert(modals.LaunchModePR) - case modals.LaunchModeVersionKey: - mode.Insert(modals.LaunchModeVersion) - } - } - go func() { - if mode.Has(modals.LaunchModeVersion) { - modals.OverwriteView(updater, create.FilterVersionView(callback, jobmanager, submissionData, httpclient, mode, false), callback, logger) - } else { - modals.OverwriteView(updater, create.PRInputView(callback, submissionData, string(create.IdentifierSelectModeView)), callback, logger) - } - - }() - return modals.SubmitPrepare(create.ModalTitle, string(create.IdentifierSelectModeView), logger) + slack.InteractionTypeViewSubmission: common.MakeModeStepHandler( + string(create.IdentifierSelectModeView), + create.ModalTitle, + create.FilterVersionView, + create.PRInputView, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/mce/create/steps/prInput.go b/pkg/slack/modals/mce/create/steps/prInput.go index 8bfd0f8e6..f854e706e 100644 --- a/pkg/slack/modals/mce/create/steps/prInput.go +++ b/pkg/slack/modals/mce/create/steps/prInput.go @@ -1,85 +1,22 @@ package steps import ( - "fmt" "net/http" - "strings" - "sync" "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/klog" ) func RegisterPRInput(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(create.IdentifierPRInputView, create.PRInputView(nil, modals.CallbackData{}, "")).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextPRInput(client, jobmanager, httpclient), + slack.InteractionTypeViewSubmission: common.MakePRInputHandler( + string(create.IdentifierPRInputView), + create.ModalTitle, + create.ThirdStepView, + )(client, jobmanager, httpclient), }) } - -func processNextPRInput(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(create.IdentifierPRInputView), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - klog.Infof("Private Metadata: %s", callback.View.PrivateMetadata) - submissionData := modals.MergeCallbackData(callback) - errorsResponse := validatePRInputView(submissionData, jobmanager) - if errorsResponse != nil { - return errorsResponse, nil - } - go modals.OverwriteView(updater, create.ThirdStepView(callback, jobmanager, httpclient, submissionData, string(create.IdentifierPRInputView)), callback, logger) - return modals.SubmitPrepare(create.ModalTitle, string(create.IdentifierPRInputView), logger) - }) -} - -func validatePRInputView(submissionData modals.CallbackData, jobmanager manager.JobManager) []byte { - prs, ok := submissionData.Input[modals.LaunchFromPR] - if !ok { - return nil - } - - var wg sync.WaitGroup - errCh := make(chan error) - - prSlice := strings.Split(prs, ",") - for _, pr := range prSlice { - wg.Add(1) - go func(pr string) { - defer wg.Done() - prParts, err := jobmanager.ResolveAsPullRequest(pr) - if prParts == nil { - errCh <- fmt.Errorf("invalid PR(s)") - } - if err != nil { - errCh <- err - } - }(pr) - } - - go func() { - wg.Wait() - close(errCh) - }() - - errors := make(map[string]string) - var prErrors []string - - for err := range errCh { - prErrors = append(prErrors, err.Error()) - } - - if len(prErrors) == 0 { - return nil - } - - errors[modals.LaunchFromPR] = strings.Join(prErrors, "; ") - response, err := modals.ValidationError(errors) - if err != nil { - klog.Warningf("failed to build validation error: %v", err) - return nil - } - - return response -} diff --git a/pkg/slack/modals/mce/create/steps/selectMinorMajor.go b/pkg/slack/modals/mce/create/steps/selectMinorMajor.go index f9446526f..bb34601fd 100644 --- a/pkg/slack/modals/mce/create/steps/selectMinorMajor.go +++ b/pkg/slack/modals/mce/create/steps/selectMinorMajor.go @@ -6,23 +6,17 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/klog" ) func RegisterSelectMinorMajor(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(create.IdentifierSelectMinorMajor, create.SelectMinorMajor(nil, httpclient, modals.CallbackData{}, "")).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextSelectMinorMajor(client, jobmanager, httpclient), - }) -} - -func processNextSelectMinorMajor(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(create.IdentifierSelectMinorMajor), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - klog.Infof("Private Metadata: %s", callback.View.PrivateMetadata) - submissionData := modals.MergeCallbackData(callback) - go modals.OverwriteView(updater, create.SelectVersionView(callback, jobmanager, httpclient, submissionData, string(create.IdentifierSelectMinorMajor)), callback, logger) - return modals.SubmitPrepare(create.ModalTitle, string(create.IdentifierSelectMinorMajor), logger) + slack.InteractionTypeViewSubmission: common.MakeSelectMinorMajorHandler( + string(create.IdentifierSelectMinorMajor), + create.ModalTitle, + create.SelectVersionView, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/mce/create/steps/selectVersion.go b/pkg/slack/modals/mce/create/steps/selectVersion.go index 94af0673c..8a8d0bf31 100644 --- a/pkg/slack/modals/mce/create/steps/selectVersion.go +++ b/pkg/slack/modals/mce/create/steps/selectVersion.go @@ -2,41 +2,22 @@ package steps import ( "net/http" - "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/openshift/ci-chat-bot/pkg/slack/modals/mce/create" - "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "k8s.io/klog" ) func RegisterSelectVersion(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { return modals.ForView(create.IdentifierSelectVersion, create.SelectVersionView(nil, jobmanager, httpclient, modals.CallbackData{}, "")).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: processNextSelectVersion(client, jobmanager, httpclient), - }) -} - -func processNextSelectVersion(updater modals.ViewUpdater, jobmanager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(string(create.IdentifierSelectVersion), func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - klog.Infof("Private Metadata: %s", callback.View.PrivateMetadata) - submissionData := modals.MergeCallbackData(callback) - mode := submissionData.MultipleSelection[modals.LaunchMode] - createWithPR := false - for _, key := range mode { - if strings.TrimSpace(key) == modals.LaunchModePRKey { - createWithPR = true - } - } - go func() { - if createWithPR { - modals.OverwriteView(updater, create.PRInputView(callback, submissionData, string(create.IdentifierSelectVersion)), callback, logger) - } else { - modals.OverwriteView(updater, create.ThirdStepView(callback, jobmanager, httpclient, submissionData, string(create.IdentifierSelectVersion)), callback, logger) - } - }() - return modals.SubmitPrepare(create.ModalTitle, string(create.IdentifierSelectVersion), logger) + slack.InteractionTypeViewSubmission: common.MakeSelectVersionHandler( + string(create.IdentifierSelectVersion), + create.ModalTitle, + create.PRInputView, + create.ThirdStepView, + )(client, jobmanager, httpclient), }) } diff --git a/pkg/slack/modals/mce/create/views.go b/pkg/slack/modals/mce/create/views.go index bc9b82f94..43c86a740 100644 --- a/pkg/slack/modals/mce/create/views.go +++ b/pkg/slack/modals/mce/create/views.go @@ -1,40 +1,18 @@ package create import ( - "encoding/json" "fmt" "net/http" - "slices" - "sort" "strings" "time" "github.com/openshift/ci-chat-bot/pkg/manager" "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" - "golang.org/x/mod/semver" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/klog" ) -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 Resolve: %v", closeErr) - } - }() - if err := json.NewDecoder(resp.Body).Decode(&acceptedReleases); err != nil { - return nil, err - } - return acceptedReleases, nil -} - func FirstStepView() slackClient.ModalViewRequest { return FirstStepViewWithData(modals.CallbackData{}) } @@ -128,7 +106,12 @@ func ThirdStepView(callback *slackClient.InteractionCallback, jobmanager manager } } - context := fmt.Sprintf("Duration: %s;Platform: %s;Version: %s;PR: %s", duration, platform, version, prs) + context := common.NewMetadataBuilder(). + Add("Duration", duration). + Add("Platform", platform). + Add("Version", version). + Add("PR", prs). + Build() // Set the previous step for back navigation data = modals.SetPreviousStep(data, previousStep) return slackClient.ModalViewRequest{ @@ -156,10 +139,6 @@ func ThirdStepView(callback *slackClient.InteractionCallback, jobmanager manager } func SelectModeView(callback *slackClient.InteractionCallback, jobmanager manager.JobManager, data modals.CallbackData) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } - klog.Infof("Callback Data: %+v", data) platform, ok := data.Input[modals.LaunchPlatform] if !ok { platform = defaultPlatform @@ -168,466 +147,109 @@ func SelectModeView(callback *slackClient.InteractionCallback, jobmanager manage if !ok { duration = defaultDuration } - metadata := fmt.Sprintf("Platform: %s; Duration: %s", platform, duration) - options := modals.BuildOptions([]string{modals.LaunchModePRKey, modals.LaunchModeVersionKey}, nil) - - // Build initial options from saved selections - var initialOptions []*slackClient.OptionBlockObject - if modes, ok := data.MultipleSelection[modals.LaunchMode]; ok { - for _, mode := range modes { - initialOptions = append(initialOptions, &slackClient.OptionBlockObject{ - Value: mode, - Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: mode}, - }) - } - } - - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, string(IdentifierInitialView)) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierSelectModeView)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch an MCE Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Select the launch mode", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchMode, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch the Cluster using:"}, - Element: &slackClient.CheckboxGroupsBlockElement{ - Type: slackClient.METCheckboxGroups, - Options: options, - InitialOptions: initialOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + metadata := common.NewMetadataBuilder(). + Add("Platform", platform). + Add("Duration", duration). + Build() + + return common.BuildSelectModeView(common.SelectModeViewConfig{ + Callback: callback, + Data: data, + ModalIdentifier: string(IdentifierSelectModeView), + Title: ModalTitle, + PreviousStep: string(IdentifierInitialView), + ContextMetadata: metadata, + }) } func FilterVersionView(callback *slackClient.InteractionCallback, jobmanager manager.JobManager, data modals.CallbackData, httpclient *http.Client, mode sets.Set[string], noneSelected bool) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } - klog.Infof("Callback Data: %+v", data) platform := data.Input[modals.LaunchPlatform] duration := data.Input[CreateDuration] - latestBuildOptions := []*slackClient.OptionBlockObject{} - _, nightly, _, err := jobmanager.ResolveImageOrVersion("nightly", "", "amd64") - if err == nil { - latestBuildOptions = append(latestBuildOptions, &slackClient.OptionBlockObject{Value: "nightly", Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: nightly}}) - } - _, ci, _, err := jobmanager.ResolveImageOrVersion("ci", "", "amd64") - if err == nil { - latestBuildOptions = append(latestBuildOptions, &slackClient.OptionBlockObject{Value: "ci", Text: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: ci}}) - } - releases, err := FetchReleases(httpclient, "amd64") - if err != nil { - klog.Warningf("failed to fetch the data from release controller: %s", err) - return modals.ErrorView("retrive valid releases from the release-controller", err) - } - var streams []string - for stream := range releases { - if platform == "hypershift-hosted" { - for _, v := range sets.List(manager.HypershiftSupportedVersions.Versions) { - if strings.HasPrefix(stream, v) || strings.Split(stream, "-")[1] == "dev" || strings.Split(stream, "-")[1] == "stable" { - streams = append(streams, stream) - break - } - } - } else { - streams = append(streams, stream) - } - - } - - sort.Strings(streams) - streamsOptions := modals.BuildOptions(streams, nil) - metadata := fmt.Sprintf("Duration: %s;Platform: %s;%s: %s", duration, platform, modals.LaunchModeContext, strings.Join(sets.List(mode), ",")) - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, string(IdentifierSelectModeView)) - view := slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierFilterVersionView)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Version Specifications", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "*Specify the _stream_ to get a list of versions to select from*", - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromStream, - Optional: true, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Specify the Stream:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: streamsOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider_section", - }, - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "\n*Alternatively:*\n*Launch using the latest Nightly or CI build*", - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromLatestBuild, - Optional: true, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "The latest build (nightly) or CI build:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: latestBuildOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider_2nd_section", - }, - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "\n*Alternatively:*\n*Launch using a _Custom_ Pull Spec*", - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromCustom, - Optional: true, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter a Custom Pull Spec:"}, - Element: &slackClient.PlainTextInputBlockElement{ - Type: slackClient.METPlainTextInput, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter a custom pull spec..."}, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } - if noneSelected { - view.Blocks.BlockSet = append([]slackClient.Block{slackClient.NewHeaderBlock(slackClient.NewTextBlockObject(slackClient.PlainTextType, ":warning: Error: At least one option must be selected :warning:", true, false))}, view.Blocks.BlockSet...) - } - return view + metadata := common.NewMetadataBuilder(). + Add("Duration", duration). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(sets.List(mode), ",")). + Build() + + return common.BuildFilterVersionView(common.VersionViewConfig{ + Callback: callback, + Data: data, + JobManager: jobmanager, + HTTPClient: httpclient, + Mode: mode, + NoneSelected: noneSelected, + ModalIdentifier: string(IdentifierFilterVersionView), + Title: ModalTitle, + PreviousStep: string(IdentifierSelectModeView), + Architecture: "amd64", // MCE clusters use amd64 + ContextMetadata: metadata, + }) } func PRInputView(callback *slackClient.InteractionCallback, data modals.CallbackData, previousStep string) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } platform := data.Input[modals.LaunchPlatform] duration := data.Input[CreateDuration] mode := data.MultipleSelection[modals.LaunchMode] - launchWithVersion := false - for _, key := range mode { - if strings.TrimSpace(key) == modals.LaunchModeVersionKey { - launchWithVersion = true - } - } - metadata := fmt.Sprintf("Duration: %s; Platform: %s;%s: %s", duration, platform, modals.LaunchModeContext, mode) - if launchWithVersion { - version := data.Input[modals.LaunchVersion] - if version == "" { - version = data.Input[modals.LaunchFromLatestBuild] - } - if version == "" { - version = data.Input[modals.LaunchFromCustom] - } - metadata = fmt.Sprintf("%s;Version: %s", metadata, version) - } - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, previousStep) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierPRInputView)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Enter A PR", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromPR, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter one or more PRs, separated by comma:"}, - Element: &slackClient.PlainTextInputBlockElement{ - Type: slackClient.METPlainTextInput, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Enter one or more PRs..."}, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + baseMetadata := common.NewMetadataBuilder(). + Add("Duration", duration). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(mode, ",")). + Build() + metadata := common.BuildPRInputMetadata(data, baseMetadata) + + return common.BuildPRInputView(common.VersionViewConfig{ + Callback: callback, + Data: data, + ModalIdentifier: string(IdentifierPRInputView), + Title: ModalTitle, + PreviousStep: previousStep, + ContextMetadata: metadata, + }) } func SelectVersionView(callback *slackClient.InteractionCallback, jobmanager manager.JobManager, httpclient *http.Client, data modals.CallbackData, previousStep string) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } - platform := data.Input[modals.LaunchPlatform] duration := data.Input[CreateDuration] mode := data.MultipleSelection[modals.LaunchMode] - selectedStream := data.Input[modals.LaunchFromStream] - selectedMajorMinor := data.Input[modals.LaunchFromMajorMinor] - metadata := fmt.Sprintf("Duration: %s; Platform: %s; %s: %s", duration, platform, modals.LaunchModeContext, mode) - releases, err := FetchReleases(httpclient, "amd64") - if err != nil { - klog.Warningf("failed to fetch the data from release controller: %s", err) - return modals.ErrorView("retrive valid releases from the release-controller", err) - } - var allTags []string - for stream, tags := range releases { - if stream == selectedStream { - for _, tag := range tags { - if strings.HasPrefix(tag, selectedMajorMinor) { - allTags = append(allTags, tag) - } - } - - } - } - if len(allTags) > 99 { - return SelectMinorMajor(callback, httpclient, data, previousStep) - } - //sort.Strings(allTags) - allTagsOptions := modals.BuildOptions(allTags, nil) - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, previousStep) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierSelectVersion)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "Select a Version", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchVersion, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select a version:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: allTagsOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "context_divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + metadata := common.NewMetadataBuilder(). + Add("Duration", duration). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(mode, ",")). + Build() + + return common.BuildSelectVersionView(common.VersionViewConfig{ + Callback: callback, + Data: data, + JobManager: jobmanager, + HTTPClient: httpclient, + ModalIdentifier: string(IdentifierSelectVersion), + Title: ModalTitle, + PreviousStep: previousStep, + Architecture: "amd64", // MCE clusters use amd64 + ContextMetadata: metadata, + }) } func SelectMinorMajor(callback *slackClient.InteractionCallback, httpclient *http.Client, data modals.CallbackData, previousStep string) slackClient.ModalViewRequest { - if callback == nil { - return slackClient.ModalViewRequest{} - } - platform := data.Input[modals.LaunchPlatform] duration := data.Input[CreateDuration] mode := data.MultipleSelection[modals.LaunchMode] selectedStream := data.Input[modals.LaunchFromStream] - metadata := fmt.Sprintf("Duration: %s; Platform: %s; %s: %s; %s: %s", duration, platform, modals.LaunchModeContext, mode, modals.LaunchFromStream, selectedStream) - releases, err := FetchReleases(httpclient, "amd64") - if err != nil { - klog.Warningf("failed to fetch the data from release controller: %s", err) - return modals.ErrorView("retrive valid releases from the release-controller", err) - } - - majorMinor := make(map[string]bool, 0) - for stream, tags := range releases { - if stream != selectedStream { - continue - } - if strings.HasPrefix(stream, modals.StableReleasesPrefix) { - for _, tag := range tags { - splitTag := strings.Split(tag, ".") - if len(splitTag) >= 2 { - majorMinor[fmt.Sprintf("%s.%s", splitTag[0], splitTag[1])] = true - } - } - - } - } - var majorMinorReleases []string - for key := range majorMinor { - if manager.HypershiftSupportedVersions.Versions.Has(key) || platform != "hypershift-hosted" { - majorMinorReleases = append(majorMinorReleases, key) - } - - } - // the x/mod/semver requires a `v` prefix for a version to be considered valid - for index, version := range majorMinorReleases { - majorMinorReleases[index] = "v" + version - } - semver.Sort(majorMinorReleases) - for index, version := range majorMinorReleases { - majorMinorReleases[index] = strings.TrimPrefix(version, "v") - } - slices.Reverse(majorMinorReleases) - majorMinorOptions := modals.BuildOptions(majorMinorReleases, nil) - // Set the previous step for back navigation - data = modals.SetPreviousStep(data, previousStep) - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(data, string(IdentifierSelectMinorMajor)), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Launch a Cluster"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Cancel"}, - Submit: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Next"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - modals.BackButtonBlock(), - &slackClient.HeaderBlock{ - Type: slackClient.MBTHeader, - Text: &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: "There are to many results from the selected Stream. Select a Minor.Major as well", - Emoji: false, - Verbatim: false, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "divider", - }, - &slackClient.InputBlock{ - Type: slackClient.MBTInput, - BlockID: modals.LaunchFromMajorMinor, - Label: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Specify the Major.Minor:"}, - Element: &slackClient.SelectBlockElement{ - Type: slackClient.OptTypeStatic, - Placeholder: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Select an entry..."}, - Options: majorMinorOptions, - }, - }, - &slackClient.DividerBlock{ - Type: slackClient.MBTDivider, - BlockID: "context_divider", - }, - &slackClient.ContextBlock{ - Type: slackClient.MBTContext, - BlockID: modals.LaunchStepContext, - ContextElements: slackClient.ContextElements{Elements: []slackClient.MixedElement{ - &slackClient.TextBlockObject{ - Type: slackClient.PlainTextType, - Text: metadata, - Emoji: false, - Verbatim: false, - }, - }}, - }, - }}, - } + metadata := common.NewMetadataBuilder(). + Add("Duration", duration). + Add("Platform", platform). + Add(modals.LaunchModeContext, strings.Join(mode, ",")). + Add(modals.LaunchFromStream, selectedStream). + Build() + + return common.BuildSelectMinorMajorView(common.VersionViewConfig{ + Callback: callback, + Data: data, + HTTPClient: httpclient, + ModalIdentifier: string(IdentifierSelectMinorMajor), + Title: ModalTitle, + PreviousStep: previousStep, + Architecture: "amd64", // MCE clusters use amd64 + ContextMetadata: metadata, + }) } diff --git a/pkg/slack/modals/mce/delete/delete.go b/pkg/slack/modals/mce/delete/delete.go index bb791de52..54b671779 100644 --- a/pkg/slack/modals/mce/delete/delete.go +++ b/pkg/slack/modals/mce/delete/delete.go @@ -17,6 +17,7 @@ func Register(client *slack.Client, jobmanager manager.JobManager) *modals.FlowW }) } +// process has custom logic and can't use MakeSimpleProcessHandler 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() { @@ -40,7 +41,7 @@ func process(updater *slack.Client, jobManager manager.JobManager) interactions. } msg, err := jobManager.DeleteMceCluster(callback.User.ID, clusterName) if err != nil { - modals.OverwriteView(updater, modals.ErrorView("deleting the Managed Cluster", err), callback, logger) + modals.OverwriteView(updater, modals.ErrorView("deleting managed cluster", err), callback, logger) } modals.OverwriteView(updater, modals.SubmissionView(title, msg), callback, logger) }() diff --git a/pkg/slack/modals/mce/delete/views.go b/pkg/slack/modals/mce/delete/views.go index af2637451..e37ea7829 100644 --- a/pkg/slack/modals/mce/delete/views.go +++ b/pkg/slack/modals/mce/delete/views.go @@ -1,25 +1,14 @@ package delete 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 terminate your running cluster", - }, - }, - }}, - } + return common.BuildSimpleView( + identifier, + title, + "Click submit to terminate your running cluster", + ) } diff --git a/pkg/slack/modals/mce/list/list.go b/pkg/slack/modals/mce/list/list.go index 1ec75849f..65a53990c 100644 --- a/pkg/slack/modals/mce/list/list.go +++ b/pkg/slack/modals/mce/list/list.go @@ -6,6 +6,7 @@ import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/sirupsen/logrus" "github.com/slack-go/slack" ) @@ -23,18 +24,7 @@ func process(updater *slack.Client, jobManager manager.JobManager, httpclient *h return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { go func() { _, beginning, elements := jobManager.ListManagedClusters("") - - 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)) - } + submission := common.BuildListResultModal(title, beginning, elements) modals.OverwriteView(updater, submission, callback, logger) }() return modals.SubmitPrepare(title, identifier, logger) diff --git a/pkg/slack/modals/mce/list/views.go b/pkg/slack/modals/mce/list/views.go index 4799dfaad..3b8d4fd9d 100644 --- a/pkg/slack/modals/mce/list/views.go +++ b/pkg/slack/modals/mce/list/views.go @@ -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 view all running MCE clusters.", - }, - }, - }}, - } + return common.BuildSimpleView( + identifier, + title, + "Click submit to view all running MCE clusters.", + ) } diff --git a/pkg/slack/modals/mce/lookup/lookup.go b/pkg/slack/modals/mce/lookup/lookup.go index 439029ae3..20a2c6fca 100644 --- a/pkg/slack/modals/mce/lookup/lookup.go +++ b/pkg/slack/modals/mce/lookup/lookup.go @@ -4,9 +4,8 @@ import ( "net/http" "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/slack-go/slack" ) @@ -14,16 +13,19 @@ const identifier = "mce_lookup" const title = "Lookup MCE Versions" func Register(client *slack.Client, jobmanager manager.JobManager, httpclient *http.Client) *modals.FlowWithViewAndFollowUps { - return modals.ForView(identifier, View()).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: process(client, jobmanager, httpclient), - }) -} - -func process(updater *slack.Client, jobManager manager.JobManager, httpclient *http.Client) interactions.Handler { - return interactions.HandlerFunc(identifier, func(callback *slack.InteractionCallback, logger *logrus.Entry) (output []byte, err error) { - go func() { - modals.OverwriteView(updater, modals.SubmissionView(title, jobManager.ListMceVersions()), callback, logger) - }() - return modals.SubmitPrepare(title, identifier, logger) - }) + return common.RegisterSimpleModal( + common.SimpleModalConfig{ + Identifier: identifier, + Title: title, + ViewFunc: View, + }, + common.MakeSimpleProcessHandler( + identifier, + title, + func(jobManager manager.JobManager, callback *slack.InteractionCallback) (string, error) { + return jobManager.ListMceVersions(), nil + }, + "listing MCE versions", + ), + )(client, jobmanager) } diff --git a/pkg/slack/modals/mce/lookup/views.go b/pkg/slack/modals/mce/lookup/views.go index 382d8eddf..ae748d80b 100644 --- a/pkg/slack/modals/mce/lookup/views.go +++ b/pkg/slack/modals/mce/lookup/views.go @@ -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 view all Openshift versions available for MCE.\nNote: CI versions may also be used, but will take longer to launch.", - }, - }, - }}, - } + return common.BuildSimpleView( + identifier, + title, + "Click submit to view all Openshift versions available for MCE.\nNote: CI versions may also be used, but will take longer to launch.", + ) } diff --git a/pkg/slack/modals/refresh/refresh.go b/pkg/slack/modals/refresh/refresh.go index ce06bdaf7..8566d78ca 100644 --- a/pkg/slack/modals/refresh/refresh.go +++ b/pkg/slack/modals/refresh/refresh.go @@ -2,9 +2,8 @@ package refresh import ( "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/openshift/ci-chat-bot/pkg/slack/modals/common" "github.com/slack-go/slack" ) @@ -12,21 +11,19 @@ const identifier = "refresh" const title = "Refresh the Status" func Register(client *slack.Client, jobmanager manager.JobManager) *modals.FlowWithViewAndFollowUps { - return modals.ForView(identifier, View()).WithFollowUps(map[slack.InteractionType]interactions.Handler{ - slack.InteractionTypeViewSubmission: process(client, jobmanager), - }) -} - -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() { - msg, err := jobManager.SyncJobForUser(callback.User.ID) - if err != nil { - modals.OverwriteView(updater, modals.ErrorView("synchronizing jobs for user", err), callback, logger) - return - } - modals.OverwriteView(updater, modals.SubmissionView(title, msg), callback, logger) - }() - return modals.SubmitPrepare(title, identifier, logger) - }) + return common.RegisterSimpleModal( + common.SimpleModalConfig{ + Identifier: identifier, + Title: title, + ViewFunc: View, + }, + common.MakeSimpleProcessHandler( + identifier, + title, + func(jobManager manager.JobManager, callback *slack.InteractionCallback) (string, error) { + return jobManager.SyncJobForUser(callback.User.ID) + }, + "synchronizing jobs for user", + ), + )(client, jobmanager) } diff --git a/pkg/slack/modals/refresh/views.go b/pkg/slack/modals/refresh/views.go index b309c4f77..d0e1b0b3f 100644 --- a/pkg/slack/modals/refresh/views.go +++ b/pkg/slack/modals/refresh/views.go @@ -1,58 +1,14 @@ package refresh 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 ResultView(msg string) slackClient.ModalViewRequest { - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Refresh the Status"}, - Close: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Close"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: msg, - }, - }, - }}, - } -} - func View() slackClient.ModalViewRequest { - return slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - PrivateMetadata: modals.CallbackDataToMetadata(modals.CallbackData{}, identifier), - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Refresh the Status"}, - 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: "If the cluster is currently marked as failed, retry fetching its credentials in case of an error", - }, - }, - }}, - } -} - -func PrepareNextStepView() *slackClient.ModalViewRequest { - return &slackClient.ModalViewRequest{ - Type: slackClient.VTModal, - Title: &slackClient.TextBlockObject{Type: slackClient.PlainTextType, Text: "Refresh the Status"}, - Blocks: slackClient.Blocks{BlockSet: []slackClient.Block{ - &slackClient.SectionBlock{ - Type: slackClient.MBTSection, - Text: &slackClient.TextBlockObject{ - Type: slackClient.MarkdownType, - Text: "Processing the next step, do not close this window...", - }, - }, - }}, - } + return common.BuildSimpleView( + identifier, + title, + "If the cluster is currently marked as failed, retry fetching its credentials in case of an error", + ) }