Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 172 additions & 34 deletions cmd/cli/app/quickstart/quickstart.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"os"
"strings"

git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/grpc"
Expand Down Expand Up @@ -253,10 +255,24 @@ func quickstartCommand(
return loadCatalog(cmd, ruleClient, profileClient, provider, project, registeredRepos)
}

// loadCatalog loads and applies the quickstart rule type and profile catalog
// using the same prompts and flow as the inline implementation.
const (
quickstartCatalogRepoURL = "https://github.com/mindersec/minder-rules-and-profiles.git"
quickstartRuleTypeFilePath = "rule-types/github/secret_scanning.yaml"
quickstartProfileFilePath = "profiles/github/profile.yaml"
)

// loadCatalog drives the catalog portion of the quickstart flow.
//
// It first asks the user to confirm creation of the initial quickstart
// resources (the secret_scanning rule type and its profile), then attempts
// to source those resources from the official minder-rules-and-profiles
// catalog repository using loadCatalogFromRepo.
//
//nolint:gocyclo // kept as a straight extraction to preserve behavior
// If cloning the catalog repository or reading/parsing its files fails for
// any reason, loadCatalog prints a warning and transparently falls back to
// runExistingFlow, which uses the embedded quickstart YAML files shipped
// with the CLI. This preserves the original quickstart behavior while
// preferring the up-to-date remote catalog when available.
func loadCatalog(
cmd *cobra.Command,
ruleClient minderv1.RuleTypeServiceClient,
Expand All @@ -275,38 +291,175 @@ func loadCatalog(
return nil
}

// Creating the rule type
cmd.Println("Creating rule type...")
if err := loadCatalogFromRepo(cmd, ruleClient, profileClient, provider, project, registeredRepos); err != nil {
cmd.Printf("Warning: failed to load quickstart catalog from %s: %v\n", quickstartCatalogRepoURL, err)
cmd.Printf("Falling back to embedded quickstart catalog.\n")
return runExistingFlow(cmd, ruleClient, profileClient, provider, project, registeredRepos)
}

return nil
}

// loadCatalogFromRepo loads the quickstart rule type and profile directly
// from the official minder-rules-and-profiles Git repository.
//
// The function clones the catalog repository in memory, opens the
// quickstart rule type and profile YAML files from the in-memory
// filesystem, parses them into protobuf/CLI profile structures, applies
// the current provider/project context, and then creates the resources via
// the RuleType and Profile gRPC services.
//
// It mirrors the prompts and output of the original quickstart flow while
// sourcing the definitions from a remote catalog instead of the embedded
// YAML. Any error is propagated to the caller so that a higher-level
// fallback (to the embedded flow) can be applied.
func loadCatalogFromRepo(
cmd *cobra.Command,
ruleClient minderv1.RuleTypeServiceClient,
profileClient minderv1.ProfileServiceClient,
provider string,
project string,
registeredRepos []string,
) error {
catalogRepo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{URL: quickstartCatalogRepoURL})
if err != nil {
return fmt.Errorf("failed to clone catalog repository: %w", err)
}

worktree, err := catalogRepo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}

fs := worktree.Filesystem

rtReader, err := fs.Open(quickstartRuleTypeFilePath)
if err != nil {
return fmt.Errorf("failed to open rule type file: %w", err)
}
defer rtReader.Close()

rt := &minderv1.RuleType{}
if err := minderv1.ParseResource(rtReader, rt); err != nil {
return fmt.Errorf("failed to parse rule type: %w", err)
}

rt.Context = &minderv1.Context{
Provider: &provider,
Project: &project,
}

ctx, cancel := getQuickstartContext(cmd.Context(), viper.GetViper())
defer cancel()

cmd.Printf("Creating rule type from remote catalog...\n")
_, err = ruleClient.CreateRuleType(ctx, &minderv1.CreateRuleTypeRequest{RuleType: rt})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() != codes.AlreadyExists {
return fmt.Errorf("error creating rule type from remote catalog: %w", err)
}
cmd.Println("Rule type secret_scanning already exists")
} else {
return cli.MessageAndError("error creating rule type", err)
}
}

yes := cli.PrintYesNoPrompt(cmd,
fmt.Sprintf(stepPromptMsgProfile, strings.Join(registeredRepos, "\n")),
"Proceed?",
"Quickstart operation cancelled.",
true)
if !yes {
return nil
}

cmd.Printf("Creating profile from remote catalog...\n")
profileReader, err := fs.Open(quickstartProfileFilePath)
if err != nil {
return fmt.Errorf("failed to open profile file: %w", err)
}
defer profileReader.Close()

p, err := profiles.ParseYAML(profileReader)
if err != nil {
return fmt.Errorf("failed to parse profile: %w", err)
}

p.Context = &minderv1.Context{
Provider: &provider,
Project: &project,
}

ctx, cancel = getQuickstartContext(cmd.Context(), viper.GetViper())
defer cancel()

alreadyExists := false
resp, err := profileClient.CreateProfile(ctx, &minderv1.CreateProfileRequest{Profile: p})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() != codes.AlreadyExists {
return cli.MessageAndError("error creating profile", err)
}
alreadyExists = true
} else {
return cli.MessageAndError("error creating profile", err)
}
}

if alreadyExists {
cmd.Println(cli.WarningBanner.Render(stepPromptMsgFinishExisting + stepPromptMsgFinishBase))
} else {
cmd.Println(cli.WarningBanner.Render(stepPromptMsgFinishOK + stepPromptMsgFinishBase))
cmd.Println("Profile details (minder profile list):")
table := profile.NewProfileRulesTable(cmd.OutOrStdout())
profile.RenderProfileRulesTable(resp.GetProfile(), table)
table.Render()
}

// Load the rule type from the embedded file system
return nil
}

// runExistingFlow contains the original quickstart catalog logic that uses
// the embedded secret_scanning rule type and profile YAML files.
//
// This function is used as a safe fallback when loading the catalog from
// the remote minder-rules-and-profiles repository fails. It recreates the
// previous Step 3 and Step 4 behavior by:
// - reading secret_scanning.yaml and profile.yaml from the embedded FS,
// - parsing them into the appropriate rule type and profile structures,
// - applying the current provider/project context, and
// - creating the resources via the corresponding gRPC services, including
// handling AlreadyExists responses and printing the final banners and
// profile details table.
func runExistingFlow(
cmd *cobra.Command,
ruleClient minderv1.RuleTypeServiceClient,
profileClient minderv1.ProfileServiceClient,
provider string,
project string,
registeredRepos []string,
) error {
cmd.Println("Creating rule type...")
reader, err := content.Open("embed/secret_scanning.yaml")
if err != nil {
return cli.MessageAndError("error opening rule type", err)
}

rt := &minderv1.RuleType{}

if err := minderv1.ParseResource(reader, rt); err != nil {
return cli.MessageAndError("error parsing rule type", err)
}

if rt.Context == nil {
rt.Context = &minderv1.Context{}
}

rt.Context = &minderv1.Context{
Provider: &provider,
Project: &project,
}

// New context so we don't time out between steps
ctx, cancel := getQuickstartContext(cmd.Context(), viper.GetViper())
defer cancel()

// Create the rule type in minder
_, err = ruleClient.CreateRuleType(ctx, &minderv1.CreateRuleTypeRequest{
RuleType: rt,
})
_, err = ruleClient.CreateRuleType(ctx, &minderv1.CreateRuleTypeRequest{RuleType: rt})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() != codes.AlreadyExists {
Expand All @@ -318,47 +471,36 @@ func loadCatalog(
}
}

// Step 4 - Confirm profile creation
yes = cli.PrintYesNoPrompt(cmd,
fmt.Sprintf(stepPromptMsgProfile, strings.Join(registeredRepos[:], "\n")),
yes := cli.PrintYesNoPrompt(cmd,
fmt.Sprintf(stepPromptMsgProfile, strings.Join(registeredRepos, "\n")),
"Proceed?",
"Quickstart operation cancelled.",
true)
if !yes {
return nil
}

// Creating the profile
cmd.Println("Creating profile...")
reader, err = content.Open("embed/profile.yaml")
if err != nil {
return cli.MessageAndError("error opening profile", err)
}

// Load the profile from the embedded file system
p, err := profiles.ParseYAML(reader)
if err != nil {
return cli.MessageAndError("error parsing profile", err)
}

if p.Context == nil {
p.Context = &minderv1.Context{}
}

p.Context = &minderv1.Context{
Provider: &provider,
Project: &project,
}

// New context so we don't time out between steps
ctx, cancel = getQuickstartContext(cmd.Context(), viper.GetViper())
defer cancel()

alreadyExists := false
// Create the profile in minder
resp, err := profileClient.CreateProfile(ctx, &minderv1.CreateProfileRequest{
Profile: p,
})
resp, err := profileClient.CreateProfile(ctx, &minderv1.CreateProfileRequest{Profile: p})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() != codes.AlreadyExists {
Expand All @@ -370,14 +512,10 @@ func loadCatalog(
}
}

// Finish - Confirm profile creation
if alreadyExists {
// Print the "profile already exists" message
cmd.Println(cli.WarningBanner.Render(stepPromptMsgFinishExisting + stepPromptMsgFinishBase))
} else {
// Print the "profile created" message
cmd.Println(cli.WarningBanner.Render(stepPromptMsgFinishOK + stepPromptMsgFinishBase))
// Print the profile create result table
cmd.Println("Profile details (minder profile list):")
table := profile.NewProfileRulesTable(cmd.OutOrStdout())
profile.RenderProfileRulesTable(resp.GetProfile(), table)
Expand Down
Loading