Skip to content

Commit

Permalink
feat(devops-1938): add joy setup command (#33)
Browse files Browse the repository at this point in the history
Co-authored-by: Mathieu Frenette <silphid@users.noreply.github.com>
  • Loading branch information
silphid and silphid authored Oct 27, 2023
1 parent 53b6201 commit ba1e134
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 56 deletions.
17 changes: 16 additions & 1 deletion cmd/joy/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package main
import (
"fmt"
"github.com/nestoca/joy/internal/config"
"github.com/nestoca/joy/internal/dependencies"
"github.com/spf13/cobra"
"os"
)

var cfg *config.Config
var configDir, catalogDir string

func main() {
rootCmd := NewRootCmd()
Expand All @@ -18,17 +20,29 @@ func main() {
}

func NewRootCmd() *cobra.Command {
var configDir, catalogDir string
setupCmd := NewSetupCmd()

cmd := &cobra.Command{
Use: "joy",
Short: "Manages project, environment and release resources as code",
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error
if cmd != setupCmd {
dependencies.AllRequiredMustBeInstalled()
}

cfg, err = config.Load(configDir, catalogDir)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}

if cmd != setupCmd {
err = config.CheckCatalogDir(cfg.CatalogDir)
if err != nil {
return err
}
}
return nil
},
}
Expand All @@ -53,6 +67,7 @@ func NewRootCmd() *cobra.Command {
// Additional commands
cmd.AddCommand(NewSecretCmd())
cmd.AddCommand(NewVersionCmd())
cmd.AddCommand(setupCmd)

return cmd
}
22 changes: 22 additions & 0 deletions cmd/joy/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"github.com/nestoca/joy/internal/setup"
"github.com/spf13/cobra"
)

func NewSetupCmd() *cobra.Command {
var catalogRepo string
cmd := &cobra.Command{
Use: "setup",
Short: "Setup joy for first time use",
Long: `Setup joy for first time use.
It prompts user for catalog directory, optionally cloning it if needed, creates config file and checks for required and optional dependencies.`,
RunE: func(cmd *cobra.Command, args []string) error {
return setup.Setup(configDir, catalogDir, catalogRepo)
},
}
cmd.Flags().StringVar(&catalogRepo, "catalog-repo", "", "URL of catalog git repo (defaults to prompting user)")
return cmd
}
13 changes: 8 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,19 @@ func Load(configDir, catalogDir string) (*Config, error) {
}
cfg.FilePath = joyrcPath

return cfg, nil
}

func CheckCatalogDir(catalogDir string) error {
// Ensure that catalog's environments directory exists
environmentsDir := filepath.Join(cfg.CatalogDir, "environments")
environmentsDir := filepath.Join(catalogDir, "environments")
if _, err := os.Stat(environmentsDir); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("no joy catalog found at %q", cfg.CatalogDir)
return fmt.Errorf("no joy catalog found at %q", catalogDir)
}
return nil, fmt.Errorf("checking for catalog directory %s: %w", cfg.CatalogDir, err)
return fmt.Errorf("checking for catalog directory %s: %w", catalogDir, err)
}

return cfg, nil
return nil
}

func LoadFile(file string) (*Config, error) {
Expand Down
59 changes: 59 additions & 0 deletions internal/dependencies/dependency.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dependencies

import (
"fmt"
"github.com/nestoca/joy/internal/style"
"os"
"os/exec"
)

type Dependency struct {
// Command that should be found in PATH
Command string

// Url to dependency's website
Url string

// IsRequired indicates whether this is a core dependency required to run joy
IsRequired bool

// RequiredBy lists which joy sub-commands require this dependency
RequiredBy []string
}

func (d *Dependency) IsInstalled() bool {
cmd := exec.Command("command", "-v", d.Command)
return cmd.Run() == nil
}

func (d *Dependency) MustBeInstalled() {
if !d.IsInstalled() {
fmt.Printf("😅 Oops! This command requires %s dependency (see: %s)\n", style.Code(d.Command), style.Link(d.Url))
os.Exit(1)
}
}

var AllRequired []*Dependency
var AllOptional []*Dependency

func Add(dep *Dependency) {
if dep.IsRequired {
AllRequired = append(AllRequired, dep)
} else {
AllOptional = append(AllOptional, dep)
}
}

func AllRequiredMustBeInstalled() {
missingRequired := false
for _, dep := range AllRequired {
if dep.IsRequired && !dep.IsInstalled() {
fmt.Printf("❌ The %s required dependency is missing (see %s).\n", style.Code(dep.Command), style.Link(dep.Url))
missingRequired = true
}
}
if missingRequired {
fmt.Println("😅 Oops! Joy requires those dependencies to operate. Please install them and try again! 🙏")
os.Exit(1)
}
}
11 changes: 11 additions & 0 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ package git
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/nestoca/joy/internal/dependencies"
"github.com/nestoca/joy/internal/style"
"os"
"os/exec"
"strings"
)

var dependency = &dependencies.Dependency{
Command: "git",
Url: "https://git-scm.com/downloads",
IsRequired: true,
}

func init() {
dependencies.Add(dependency)
}

func Run(dir string, args []string) error {
args = append([]string{"-C", dir}, args...)
cmd := exec.Command("git", args...)
Expand Down
20 changes: 13 additions & 7 deletions internal/git/pr/github/gh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ package github
import (
"errors"
"fmt"
"github.com/nestoca/joy/internal/dependencies"
"github.com/nestoca/joy/internal/style"
"os"
"os/exec"
"regexp"
"strings"
)

var dependency = &dependencies.Dependency{
Command: "gh",
Url: "/~https://github.com/cli/cli",
IsRequired: true,
}

func init() {
dependencies.Add(dependency)
}

// executeInteractively runs gh command with given args with full forwarding of stdin, stdout and stderr.
func executeInteractively(args ...string) error {
err := EnsureInstalledAndAuthenticated()
Expand Down Expand Up @@ -44,15 +55,10 @@ func executeAndGetOutput(args ...string) (string, error) {
}

func EnsureInstalledAndAuthenticated() error {
cmd := exec.Command("command", "-v", "gh")
err := cmd.Run()
if err != nil {
fmt.Println("🤓 This command requires the gh cli.\nSee: /~https://github.com/cli/cli")
return errors.New("missing gh cli dependency")
}
dependency.MustBeInstalled()

// Check if user is logged in
cmd = exec.Command("gh", "auth", "status")
cmd := exec.Command("gh", "auth", "status")
output, err := cmd.CombinedOutput()
outputStr := string(output)
if err != nil {
Expand Down
37 changes: 16 additions & 21 deletions internal/jac/jac.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package jac

import (
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/nestoca/joy/api/v1alpha1"
"github.com/nestoca/joy/internal/dependencies"
"github.com/nestoca/joy/internal/git"
"github.com/nestoca/joy/internal/release/cross"
"github.com/nestoca/joy/internal/style"
Expand All @@ -14,13 +14,21 @@ import (
"strings"
)

var dependency = &dependencies.Dependency{
Command: "jac",
Url: "/~https://github.com/nestoca/jac",
IsRequired: false,
RequiredBy: []string{"project owners", "release owners"},
}

func init() {
dependencies.Add(dependency)
}

func ListProjectPeople(catalogDir string, extraArgs []string) error {
err := ensureJacCliInstalled()
if err != nil {
return err
}
dependency.MustBeInstalled()

err = git.EnsureCleanAndUpToDateWorkingCopy(catalogDir)
err := git.EnsureCleanAndUpToDateWorkingCopy(catalogDir)
if err != nil {
return err
}
Expand All @@ -45,12 +53,9 @@ func ListProjectPeople(catalogDir string, extraArgs []string) error {
}

func ListReleasePeople(catalogDir string, extraArgs []string) error {
err := ensureJacCliInstalled()
if err != nil {
return err
}
dependency.MustBeInstalled()

err = git.EnsureCleanAndUpToDateWorkingCopy(catalogDir)
err := git.EnsureCleanAndUpToDateWorkingCopy(catalogDir)
if err != nil {
return err
}
Expand Down Expand Up @@ -109,16 +114,6 @@ func listPeopleWithGroups(groups []string, extraArgs []string) error {
return nil
}

func ensureJacCliInstalled() error {
cmd := exec.Command("command", "-v", "jac")
err := cmd.Run()
if err != nil {
fmt.Println("🤓 This command requires the jac cli.\nSee: /~https://github.com/nestoca/jac")
return errors.New("missing jac cli dependency")
}
return nil
}

func selectProject(projects []*v1alpha1.Project) (*v1alpha1.Project, error) {
var selectedIndex int
err := survey.AskOne(&survey.Select{
Expand Down
2 changes: 1 addition & 1 deletion internal/release/promote/interactive_prompt_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (i *InteractivePromptProvider) PrintBranchCreated(branchName, message strin
}

func (i *InteractivePromptProvider) PrintPullRequestCreated(url string) {
fmt.Printf("✅ Created pull request: %s\n", style.Hyperlink(url))
fmt.Printf("✅ Created pull request: %s\n", style.Link(url))
}

func (i *InteractivePromptProvider) PrintCanceled() {
Expand Down
28 changes: 13 additions & 15 deletions internal/secret/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package secret

import (
"encoding/base64"
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/nestoca/joy/internal/dependencies"
"github.com/nestoca/joy/internal/environment"
"github.com/nestoca/joy/internal/style"
"github.com/nestoca/joy/internal/yml"
Expand All @@ -13,11 +13,19 @@ import (
"strings"
)

var kubectlDependency = &dependencies.Dependency{
Command: "kubectl",
Url: "https://kubernetes.io/docs/tasks/tools/#kubectl",
IsRequired: false,
RequiredBy: []string{"sealed-secret import"},
}

func init() {
dependencies.Add(kubectlDependency)
}

func ImportCert() error {
err := ensureKubectlInstalled()
if err != nil {
return err
}
kubectlDependency.MustBeInstalled()

// Select kube context
context, err := selectKubeContext()
Expand Down Expand Up @@ -108,13 +116,3 @@ func selectKubeContext() (string, error) {
}
return contexts[selectedIndex], nil
}

func ensureKubectlInstalled() error {
cmd := exec.Command("command", "-v", "kubectl")
err := cmd.Run()
if err != nil {
fmt.Println("🤓 This command requires kubectl cli to be installed: https://kubernetes.io/docs/tasks/tools/#kubectl")
return errors.New("missing kubectl cli dependency")
}
return nil
}
Loading

0 comments on commit ba1e134

Please sign in to comment.