diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..047f46e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +copr-cli +copr-tool \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..76bb340 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Test", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/cmd", + "args": [ + "" + ] + }, + { + "name": "List", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "args": ["list"] + }, + { + "name": "Prune", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "args": ["prune"] + }, + { + "name": "Enable", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "args": ["enable", "kylegospo/bazzite"] + }, + { + "name": "Disable", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "args": ["disable", "kylegospo/bazzite"] + }, + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3304163 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.testFlags": [ + "-v", + "-coverpkg=all" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 1b1b47d..bd11dac 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # copr-tool -CLI app for managing COPR repos, written in Go. +CLI app for managing Copr repos, written in Go. ```shell Usage: copr-tool [OPTION] [REPO...] Options: - enable Add or enable one or more COPR repositories. - remove Remove one or more COPR repositories. - list List all (enabled and disabled) COPR repositories in your repo folder. - disable Disable one or more COPR repositories without deleting the repository files. + enable Add or enable one or more Copr repositories. + remove Remove one or more Copr repositories. + list List all (enabled and disabled) Copr repositories in your repo folder. + disable Disable one or more Copr repositories without deleting the repository files. help Display help text. Arguments: diff --git a/app/app.go b/app/app.go deleted file mode 100644 index 781f3e3..0000000 --- a/app/app.go +++ /dev/null @@ -1,142 +0,0 @@ -package app - -import ( - "fmt" - "io" - "log" - "net/http" - "net/url" - "os" - "strings" - - "github.com/go-ini/ini" -) - -const ( - CoprUrl string = "https://copr.fedorainfracloud.org/coprs/" - CoprHost string = "copr.fedorainfracloud.org" - ReposDir string = "/etc/yum.repos.d/" - Enabled RepoState = "enabled=1" - Disabled RepoState = "enabled=0" -) - -func NewCoprRepo(repoPath string) (CoprRepo, error) { - repo := CoprRepo{ - User: strings.Split(repoPath, "/")[0], - Project: strings.Split(repoPath, "/")[1], - } - return repo, nil -} - -func FedoraReleaseVersion() string { - osRelease, err := ini.Load("/etc/os-release") - if err != nil { - log.Fatal("Fail to read file: ", err) - } - - return osRelease.Section("").Key("VERSION_ID").String() -} - -func RepoFileUrl(r CoprRepo) *url.URL { - fedoraRelease := "fedora-" + FedoraReleaseVersion() - repoName := r.User + "-" + r.Project + "-" + fedoraRelease + ".repo" - base, err := url.Parse(CoprUrl) - if err != nil { - log.Fatal(err) - } - repoUrl := base.JoinPath(r.User, r.Project, "repo", fedoraRelease, repoName) - return repoUrl -} - -func RepoFileName(r CoprRepo) string { - fileName := strings.Join([]string{"_copr", CoprHost, r.User, r.Project + ".repo"}, ":") - return fileName -} - -func RepoFilePath(r CoprRepo) string { - return ReposDir + RepoFileName(r) -} - -func RepoExists(r CoprRepo) bool { - _, err := os.Stat(RepoFilePath(r)) - return !os.IsNotExist(err) -} - -func GetLocalRepoFileLines(r CoprRepo) ([]string, error) { - repoFile := RepoFilePath(r) - contents, err := os.ReadFile(repoFile) - if err != nil { - return nil, err - } - - return strings.Split(string(contents), "\n"), nil -} - -func WriteRepoToFile(r CoprRepo, content []byte) error { - err := os.WriteFile(RepoFilePath(r), content, 0644) - if err != nil { - return err - } - return nil -} - -func ToggleRepo(r CoprRepo, desiredState RepoState) error { - fileLines, err := GetLocalRepoFileLines(r) - if err != nil { - return err - } - var statusMessage string - if desiredState == Enabled { - statusMessage = "enabled" - } else { - statusMessage = "disabled" - } - - for i, line := range fileLines { - if strings.Contains(line, "enabled=") { - if line == string(desiredState) { - fmt.Printf("Repository is already %s.\n", statusMessage) - return nil - } else { - fileLines[i] = string(desiredState) - } - } - } - output := strings.Join(fileLines, "\n") - err = WriteRepoToFile(r, []byte(output)) - if err != nil { - return err - } - fmt.Printf("Repository %s/%s %s.\n", r.User, r.Project, statusMessage) - return nil -} - -func AddRepo(r CoprRepo) error { - resp, err := http.Get(RepoFileUrl(r).String()) - if err != nil { - return err - } - output, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - err = WriteRepoToFile(r, []byte(output)) - if err != nil { - return err - } - fmt.Printf("Repository %s/%s added.\n", r.User, r.Project) - return nil -} - -func DeleteRepo(r CoprRepo) error { - if RepoExists(r) { - err := os.Remove(RepoFilePath(r)) - if err != nil { - return err - } - fmt.Printf("Repository %s/%s deleted.\n", r.User, r.Project) - } else { - fmt.Printf("Repository %s/%s does not exist locally. Nothing to delete.\n", r.User, r.Project) - } - return nil -} diff --git a/app/types.go b/app/types.go deleted file mode 100644 index 1f3a27f..0000000 --- a/app/types.go +++ /dev/null @@ -1,10 +0,0 @@ -package app - -type ( - RepoState string -) - -type CoprRepo struct { - User string - Project string -} diff --git a/cmd/disable.go b/cmd/disable.go index 9c49ace..a43da5f 100644 --- a/cmd/disable.go +++ b/cmd/disable.go @@ -4,39 +4,31 @@ Copyright © 2024 NAME HERE package cmd import ( - "errors" "fmt" - "io/fs" - "os" + "io" + "github.com/spf13/afero" "github.com/spf13/cobra" - "github.com/trgeiger/copr-tool/app" + "github.com/trgeiger/copr-tool/internal/app" ) -// disableCmd represents the disable command -var disableCmd = &cobra.Command{ - Use: "disable", - Args: cobra.MinimumNArgs(1), - Short: "Disable one or more COPR repositories without removing their configuration files.", - Run: func(cmd *cobra.Command, args []string) { - for _, arg := range args { - repo, err := app.NewCoprRepo(arg) - if err != nil { - fmt.Println(err) - } - err = app.ToggleRepo(repo, app.Disabled) - if err != nil { - if errors.Is(err, fs.ErrPermission) { - fmt.Printf("This command must be run with superuser privileges.\nError: %s\n", err) +func NewDisableCmd(fs afero.Fs, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "disable", + Args: cobra.MinimumNArgs(1), + Short: "Disable one or more Copr repositories without uninstalling them.", + Run: func(cmd *cobra.Command, args []string) { + for _, arg := range args { + repo, err := app.NewCoprRepo(arg) + if err != nil { + fmt.Fprintln(out, err) } else { - fmt.Println(err) + err = app.ToggleRepo(repo, fs, out, app.Disabled) + if err != nil { + app.SudoMessage(err, out) + } } - os.Exit(1) } - } - }, -} - -func init() { - rootCmd.AddCommand(disableCmd) + }, + } } diff --git a/cmd/disable_test.go b/cmd/disable_test.go new file mode 100644 index 0000000..03612e2 --- /dev/null +++ b/cmd/disable_test.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/trgeiger/copr-tool/internal/testutil" +) + +func TestDisableCmd(t *testing.T) { + tests := []struct { + name string + args []string + repoFiles [][]string // format: file/reponame, test directory folder + otherFiles [][]string // format: filename, path, test directory folder + expected string + }{ + { + name: "Disable invalid repo name", + args: []string{ + "copr-tool", + }, + expected: "invalid repository name: copr-tool\n", + }, + { + name: "Repo does not exist", + args: []string{ + "example/example", + }, + expected: "repository example/example is not installed\n", + }, + { + name: "Repo already exists and already disabled", + args: []string{ + "kylegospo/bazzite", + }, + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "disabled"}, + }, + expected: "Repository kylegospo/bazzite is already disabled.\n", + }, + { + name: "Repo already exists but not disabled", + args: []string{ + "kylegospo/bazzite", + }, + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "enabled"}, + }, + expected: "Repository kylegospo/bazzite disabled.\n", + }, + } + + for _, test := range tests { + + b := new(bytes.Buffer) + fs := testutil.AssembleTestFs(test.repoFiles, test.otherFiles) + cmd := NewDisableCmd(fs, b) + cmd.SetOut(b) + cmd.SetArgs(test.args) + + cmd.Execute() + + if b.String() != test.expected { + t.Fatalf("Test \"%s\" failed", test.name) + } + } +} diff --git a/cmd/enable.go b/cmd/enable.go index 8b46787..70c25f0 100644 --- a/cmd/enable.go +++ b/cmd/enable.go @@ -4,69 +4,51 @@ Copyright © 2024 NAME HERE package cmd import ( - "errors" "fmt" - "io/fs" - "log" + "io" "net/http" - "os" + "github.com/spf13/afero" "github.com/spf13/cobra" - "github.com/trgeiger/copr-tool/app" + "github.com/trgeiger/copr-tool/internal/app" ) -// addCmd represents the add command -var enableCmd = &cobra.Command{ - Use: "enable", - Aliases: []string{"add"}, - Args: cobra.MinimumNArgs(1), - Short: "Enable or add one or more COPR repositories.", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - for _, arg := range args { - repo, err := app.NewCoprRepo(arg) - if err != nil { - fmt.Println(err) - } - err = enableRepo(repo) - if err != nil { - if errors.Is(err, fs.ErrPermission) { - fmt.Printf("This command must be run with superuser privileges.\nError: %s\n", err) - } else { - fmt.Println(err) - } - os.Exit(1) - } - } - }, -} - -func verifyCoprRepo(r app.CoprRepo) error { - _, err := http.Get(app.RepoFileUrl(r).String()) +func verifyCoprRepo(r *app.CoprRepo, fs afero.Fs) error { + resp, err := http.Get(r.RepoUrl()) if err != nil { return err } - + if resp.StatusCode == 404 { + return fmt.Errorf("repository does not exist, %s returned 404", r.RepoUrl()) + } + resp, err = http.Get(r.RepoConfigUrl(fs)) + if err != nil { + return err + } + if resp.StatusCode == 404 { + return fmt.Errorf("repository %s does not support Fedora release %s", r.Name(), app.FedoraReleaseVersion(fs)) + } return nil } -func enableRepo(r app.CoprRepo) error { - if verifyCoprRepo(r) != nil { - log.Fatal("Repository verification failed. Double-check repository name.") +func enableRepo(r *app.CoprRepo, fs afero.Fs, out io.Writer) error { + if err := verifyCoprRepo(r, fs); err != nil { + return err + } + err := r.FindLocalFiles(fs) + if err != nil { + return err } - if app.RepoExists(r) { - err := app.ToggleRepo(r, app.Enabled) + if r.LocalFileExists(fs) { + err := app.ToggleRepo(r, fs, out, app.Enabled) if err != nil { + app.SudoMessage(err, out) + return err } return nil } else { - err := app.AddRepo(r) + err := app.AddRepo(r, fs, out) if err != nil { return err } @@ -74,7 +56,30 @@ func enableRepo(r app.CoprRepo) error { return nil } -func init() { - rootCmd.AddCommand(enableCmd) - +func NewEnableCmd(fs afero.Fs, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "enable", + Aliases: []string{"add"}, + Args: cobra.MinimumNArgs(1), + Short: "Enable or add one or more Copr repositories.", + Long: `A longer description that spans multiple lines and likely contains examples + and usage of using your command. For example: + + Cobra is a CLI library for Go that empowers applications. + This application is a tool to generate the needed files + to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + for _, arg := range args { + repo, err := app.NewCoprRepo(arg) + if err != nil { + fmt.Fprintln(out, err) + } else { + err = enableRepo(repo, fs, out) + if err != nil { + app.SudoMessage(err, out) + } + } + } + }, + } } diff --git a/cmd/enable_test.go b/cmd/enable_test.go new file mode 100644 index 0000000..8b0d4eb --- /dev/null +++ b/cmd/enable_test.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/trgeiger/copr-tool/internal/testutil" +) + +func TestEnableCmd(t *testing.T) { + tests := []struct { + name string + args []string + repoFiles [][]string // format: file/reponame, test directory folder + otherFiles [][]string // format: filename, path, test directory folder + expected string + }{ + { + name: "Add valid repo", + args: []string{ + "kylegospo/bazzite", + }, + expected: "Repository kylegospo/bazzite added.\n", + otherFiles: [][]string{ + {"os-release", "/etc/", "f40"}, + }, + }, + { + name: "Add invalid repo name", + args: []string{ + "copr-tool", + }, + expected: "invalid repository name: copr-tool\n", + }, + { + name: "Repo does not exist", + args: []string{ + "example/example", + }, + expected: "repository does not exist, https://copr.fedorainfracloud.org/coprs/example/example returned 404\n", + }, + { + name: "Repo does not support Fedora version", + args: []string{ + "kylegospo/bazzite", + }, + otherFiles: [][]string{ + {"os-release", "/etc/", "f30"}, + }, + expected: "repository kylegospo/bazzite does not support Fedora release 30\n", + }, + { + name: "Repo already exists and already enabled", + args: []string{ + "kylegospo/bazzite", + }, + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "enabled"}, + }, + expected: "Repository kylegospo/bazzite is already enabled.\n", + }, + { + name: "Repo already exists but not enabled", + args: []string{ + "kylegospo/bazzite", + }, + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "disabled"}, + }, + expected: "Repository kylegospo/bazzite enabled.\n", + }, + } + + for _, test := range tests { + + b := new(bytes.Buffer) + fs := testutil.AssembleTestFs(test.repoFiles, test.otherFiles) + cmd := NewEnableCmd(fs, b) + cmd.SetOut(b) + cmd.SetArgs(test.args) + + cmd.Execute() + + if b.String() != test.expected { + t.Fatalf("Test: \"%s\" failed", test.name) + } + } + +} diff --git a/cmd/list.go b/cmd/list.go index c111e5d..1dd0fbd 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -4,65 +4,44 @@ Copyright © 2024 NAME HERE package cmd import ( - "bufio" "fmt" - "os" - "strings" + "io" + "github.com/spf13/afero" "github.com/spf13/cobra" - "github.com/trgeiger/copr-tool/app" + "github.com/trgeiger/copr-tool/internal/app" ) -// listCmd represents the list command -var listCmd = &cobra.Command{ - Use: "list", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - listRepos() - }, -} - -func listRepos() error { - files, err := os.ReadDir(app.ReposDir) - if err != nil { - return err - } - - for _, file := range files { - if !file.IsDir() { - ioFile, err := os.Open(app.ReposDir + file.Name()) - +func NewListCmd(fs afero.Fs, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List installed Copr repositories", + Long: `A longer description that spans multiple lines and likely contains examples + and usage of using your command. For example: + + Cobra is a CLI library for Go that empowers applications. + This application is a tool to generate the needed files + to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + repos, err := app.GetAllRepos(fs, out) if err != nil { - return err + fmt.Fprintf(out, "Error when retrieving locally installed repositories: %s", err) } - - scanner := bufio.NewScanner(ioFile) - for scanner.Scan() { - if strings.Contains(scanner.Text(), "[copr:copr") { - t := strings.Split(strings.Trim(scanner.Text(), "[]"), ":") - r, _ := app.NewCoprRepo(t[len(t)-2] + "/" + t[len(t)-1]) - properFileName := app.RepoFileName(r) - if file.Name() != properFileName { - fmt.Printf("Repository %s detected with non-standard repository file name.", r.User+"/"+r.Project) + if len(repos) == 0 { + fmt.Fprintln(out, "No installed Copr repositories.") + } else { + showDupesMessage := false + for _, r := range repos { + r.FindLocalFiles(fs) + if len(r.LocalFiles) > 1 { + showDupesMessage = true } - fmt.Println(strings.Join(t[len(t)-2:], "/")) - break + fmt.Fprintln(out, r.Name()) + } + if showDupesMessage { + fmt.Fprintln(out, "\nDuplicate entries found. Consider running the prune command.") } } - if err := scanner.Err(); err != nil { - fmt.Fprintln(os.Stderr, "Issue reading repo files: ", err) - } - } + }, } - return nil -} - -func init() { - rootCmd.AddCommand(listCmd) } diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..26cccd8 --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/trgeiger/copr-tool/internal/testutil" +) + +func TestListCmd(t *testing.T) { + tests := []struct { + name string + repoFiles [][]string // format: file/reponame, test directory folder + otherFiles [][]string // format: filename, path, test directory folder + expected string + }{ + { + name: "List existing repos", + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "enabled"}, + {"_copr:copr.fedorainfracloud.org:bieszczaders:kernel-cachyos.repo", "enabled"}, + }, + expected: "bieszczaders/kernel-cachyos\nkylegospo/bazzite\n", + }, + { + name: "No repos to list", + expected: "No installed Copr repositories.\n", + }, + } + + for _, test := range tests { + + b := new(bytes.Buffer) + fs := testutil.AssembleTestFs(test.repoFiles, test.otherFiles) + cmd := NewListCmd(fs, b) + cmd.SetOut(b) + + cmd.Execute() + + if b.String() != test.expected { + t.Fatalf("Test \"%s\" failed", test.name) + } + } + +} diff --git a/cmd/prune.go b/cmd/prune.go new file mode 100644 index 0000000..c1d93a5 --- /dev/null +++ b/cmd/prune.go @@ -0,0 +1,58 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/trgeiger/copr-tool/internal/app" +) + +// pruneCmd represents the prune command +// var pruneCmd = &cobra.Command{ +// Use: "prune", +// Short: "A brief description of your command", +// Long: `A longer description that spans multiple lines and likely contains examples +// and usage of using your command. For example: + +// Cobra is a CLI library for Go that empowers applications. +// This application is a tool to generate the needed files +// to quickly create a Cobra application.`, +// Run: func(cmd *cobra.Command, args []string) { +// fmt.Fprintln(out, "prune called") +// }, +// } + +func NewPruneCmd(fs afero.Fs, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "prune", + Short: "Remove duplicate repository configurations.", + Run: func(cmd *cobra.Command, args []string) { + repos, err := app.GetAllRepos(fs, out) + if err != nil { + fmt.Fprintf(out, "Error when retrieving locally installed repositories: %s", err) + os.Exit(1) + } + pruneCount := 0 + for _, r := range repos { + r.FindLocalFiles(fs) + pruned, err := r.PruneDuplicates(fs, out) + if pruned && err == nil { + pruneCount++ + } else if pruned && err != nil { + fmt.Fprintf(out, "Pruning attempted on %s but encountered error: %s", r.Name(), err) + } else if err != nil { + fmt.Fprintf(out, "Error encountered: %s", err) + } + } + if pruneCount == 0 { + fmt.Fprintln(out, "Nothing to prune.") + } + }, + } +} diff --git a/cmd/prune_test.go b/cmd/prune_test.go new file mode 100644 index 0000000..8aeabbf --- /dev/null +++ b/cmd/prune_test.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/trgeiger/copr-tool/internal/testutil" +) + +func TestPruneCmd(t *testing.T) { + tests := []struct { + name string + repoFiles [][]string // format: file/reponame, test directory folder + otherFiles [][]string // format: filename, path, test directory folder + expected string + }{ + { + name: "No repositories installed", + expected: "Nothing to prune.\n", + }, + { + name: "Remove 1 duplicate", + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "enabled"}, + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy.repo", "enabled"}, + }, + expected: "Removed 1 duplicate entry for kylegospo/bazzite.\n", + }, + { + name: "Remove multiple duplicates", + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "enabled"}, + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy.repo", "enabled"}, + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy2.repo", "enabled"}, + }, + expected: "Removed 2 duplicate entries for kylegospo/bazzite.\n", + }, + } + + for _, test := range tests { + + b := new(bytes.Buffer) + fs := testutil.AssembleTestFs(test.repoFiles, test.otherFiles) + cmd := NewPruneCmd(fs, b) + cmd.SetOut(b) + + cmd.Execute() + + if b.String() != test.expected { + t.Fatalf("Test \"%s\" failed", test.name) + } + } +} diff --git a/cmd/remove.go b/cmd/remove.go index 8b8b081..9d75e08 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -4,40 +4,32 @@ Copyright © 2024 NAME HERE package cmd import ( - "errors" "fmt" - "io/fs" - "os" + "io" + "github.com/spf13/afero" "github.com/spf13/cobra" - "github.com/trgeiger/copr-tool/app" + "github.com/trgeiger/copr-tool/internal/app" ) -// removeCmd represents the remove command -var removeCmd = &cobra.Command{ - Use: "remove", - Aliases: []string{"delete"}, - Args: cobra.MinimumNArgs(1), - Short: "Remove one or more COPR repositories' configuration files.", - Run: func(cmd *cobra.Command, args []string) { - for _, arg := range args { - repo, err := app.NewCoprRepo(arg) - if err != nil { - fmt.Println(err) - } - err = app.DeleteRepo(repo) - if err != nil { - if errors.Is(err, fs.ErrPermission) { - fmt.Printf("This command must be run with superuser privileges.\nError: %s\n", err) +func NewRemoveCmd(fs afero.Fs, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "remove", + Aliases: []string{"delete"}, + Args: cobra.MinimumNArgs(1), + Short: "Uninstall one or more Copr repositories.", + Run: func(cmd *cobra.Command, args []string) { + for _, arg := range args { + repo, err := app.NewCoprRepo(arg) + if err != nil { + fmt.Fprintln(out, err) } else { - fmt.Println(err) + err = app.DeleteRepo(repo, fs, out) + if err != nil { + app.SudoMessage(err, out) + } } - os.Exit(1) } - } - }, -} - -func init() { - rootCmd.AddCommand(removeCmd) + }, + } } diff --git a/cmd/remove_test.go b/cmd/remove_test.go new file mode 100644 index 0000000..a4048bd --- /dev/null +++ b/cmd/remove_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/trgeiger/copr-tool/internal/testutil" +) + +func TestRemoveCmd(t *testing.T) { + tests := []struct { + name string + args []string + repoFiles [][]string // format: file/reponame, test directory folder + otherFiles [][]string // format: filename, path, test directory folder + expected string + }{ + { + name: "Remove invalid repo name", + args: []string{ + "copr-tool", + }, + expected: "invalid repository name: copr-tool\n", + }, + { + name: "Remove uninstalled repo", + args: []string{ + "example/example", + }, + expected: "Repository example/example does not exist locally. Nothing to delete.\n", + }, + { + name: "Remove installed repo", + args: []string{ + "kylegospo/bazzite", + }, + repoFiles: [][]string{ + {"_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo", "enabled"}, + }, + expected: "Repository kylegospo/bazzite deleted.\n", + }, + } + + for _, test := range tests { + + b := new(bytes.Buffer) + fs := testutil.AssembleTestFs(test.repoFiles, test.otherFiles) + cmd := NewRemoveCmd(fs, b) + cmd.SetOut(b) + cmd.SetArgs(test.args) + + cmd.Execute() + + if b.String() != test.expected { + t.Fatalf("Test \"%s\" failed", test.name) + } + } +} diff --git a/cmd/root.go b/cmd/root.go index 7156d13..fe30ea5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,37 +5,68 @@ package cmd import ( "fmt" + "io" "os" + "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/viper" ) -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "copr-tool", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() +func Execute(fs afero.Fs) { + + viper.SetConfigName("os-release") + viper.SetConfigType("ini") + viper.AddConfigPath("/etc/") + viper.SetFs(fs) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + panic(fmt.Errorf("could not find /etc/os-release, copr-tool only functions on Fedora Linux systems: %w", err)) + + } else { + panic(fmt.Errorf("unknown fatal error: %w", err)) + } + } + if viper.Get("default.id") != "fedora" { + fmt.Println("Non-Fedora distribution detected. Copr tool only functions on Fedora Linux.") + os.Exit(1) + } + + cmd, err := NewRootCmd(fs, os.Stdout) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + err = cmd.Execute() if err != nil { fmt.Println(err) os.Exit(1) } } +func NewRootCmd(fs afero.Fs, out io.Writer) (*cobra.Command, error) { + + cmd := &cobra.Command{ + Use: "copr-tool", + Short: "A command line tool for managing Copr repositories", + } + + cmd.AddCommand( + NewDisableCmd(fs, out), + NewEnableCmd(fs, out), + NewListCmd(fs, out), + NewPruneCmd(fs, out), + NewRemoveCmd(fs, out), + ) + + return cmd, nil +} + func init() { + // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. @@ -44,5 +75,4 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/test/disabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo b/cmd/test/disabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo new file mode 100644 index 0000000..c83c771 --- /dev/null +++ b/cmd/test/disabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo @@ -0,0 +1,10 @@ +[copr:copr.fedorainfracloud.org:kylegospo:bazzite] +name=Copr repo for bazzite owned by kylegospo +baseurl=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/fedora-$releasever-$basearch/ +type=rpm-md +skip_if_unavailable=True +gpgcheck=1 +gpgkey=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/pubkey.gpg +repo_gpgcheck=0 +enabled=0 +enabled_metadata=1 diff --git a/cmd/test/enabled/_copr:copr.fedorainfracloud.org:bieszczaders:kernel-cachyos.repo b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:bieszczaders:kernel-cachyos.repo new file mode 100644 index 0000000..d273a03 --- /dev/null +++ b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:bieszczaders:kernel-cachyos.repo @@ -0,0 +1,10 @@ +[copr:copr.fedorainfracloud.org:bieszczaders:kernel-cachyos] +name=Copr repo for kernel-cachyos owned by bieszczaders +baseurl=https://download.copr.fedorainfracloud.org/results/bieszczaders/kernel-cachyos/fedora-$releasever-$basearch/ +type=rpm-md +skip_if_unavailable=True +gpgcheck=1 +gpgkey=https://download.copr.fedorainfracloud.org/results/bieszczaders/kernel-cachyos/pubkey.gpg +repo_gpgcheck=0 +enabled=1 +enabled_metadata=1 diff --git a/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy.repo b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy.repo new file mode 100644 index 0000000..6db2e69 --- /dev/null +++ b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy.repo @@ -0,0 +1,10 @@ +[copr:copr.fedorainfracloud.org:kylegospo:bazzite] +name=Copr repo for bazzite owned by kylegospo +baseurl=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/fedora-$releasever-$basearch/ +type=rpm-md +skip_if_unavailable=True +gpgcheck=1 +gpgkey=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/pubkey.gpg +repo_gpgcheck=0 +enabled=1 +enabled_metadata=1 diff --git a/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy2.repo b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy2.repo new file mode 100644 index 0000000..6db2e69 --- /dev/null +++ b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite-copy2.repo @@ -0,0 +1,10 @@ +[copr:copr.fedorainfracloud.org:kylegospo:bazzite] +name=Copr repo for bazzite owned by kylegospo +baseurl=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/fedora-$releasever-$basearch/ +type=rpm-md +skip_if_unavailable=True +gpgcheck=1 +gpgkey=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/pubkey.gpg +repo_gpgcheck=0 +enabled=1 +enabled_metadata=1 diff --git a/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo new file mode 100644 index 0000000..6db2e69 --- /dev/null +++ b/cmd/test/enabled/_copr:copr.fedorainfracloud.org:kylegospo:bazzite.repo @@ -0,0 +1,10 @@ +[copr:copr.fedorainfracloud.org:kylegospo:bazzite] +name=Copr repo for bazzite owned by kylegospo +baseurl=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/fedora-$releasever-$basearch/ +type=rpm-md +skip_if_unavailable=True +gpgcheck=1 +gpgkey=https://download.copr.fedorainfracloud.org/results/kylegospo/bazzite/pubkey.gpg +repo_gpgcheck=0 +enabled=1 +enabled_metadata=1 diff --git a/cmd/test/f30/os-release b/cmd/test/f30/os-release new file mode 100644 index 0000000..3859919 --- /dev/null +++ b/cmd/test/f30/os-release @@ -0,0 +1,4 @@ +NAME="Fedora Linux" +ID=fedora +VERSION_ID=30 +PLATFORM_ID="platform:f30" diff --git a/cmd/test/f40/os-release b/cmd/test/f40/os-release new file mode 100644 index 0000000..c0a9c58 --- /dev/null +++ b/cmd/test/f40/os-release @@ -0,0 +1,4 @@ +NAME="Fedora Linux" +ID=fedora +VERSION_ID=40 +PLATFORM_ID="platform:f40" diff --git a/go.mod b/go.mod index 7513af0..7b96645 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,30 @@ module github.com/trgeiger/copr-tool go 1.22.2 require ( - github.com/go-ini/ini v1.67.0 + github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 ) require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 290490d..88f6d97 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,76 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/copr-repo.go b/internal/app/copr-repo.go new file mode 100644 index 0000000..b677a8f --- /dev/null +++ b/internal/app/copr-repo.go @@ -0,0 +1,123 @@ +package app + +import ( + "fmt" + "io" + "log" + "net/url" + "os" + "regexp" + "strings" + + "github.com/spf13/afero" +) + +type ( + RepoState string +) + +type CoprRepo struct { + User string + Project string + LocalFiles []string +} + +func NewCoprRepo(repoName string) (*CoprRepo, error) { + if matched, _ := regexp.MatchString(`\w*\/\w*`, repoName); !matched { + return nil, fmt.Errorf("invalid repository name: %s", repoName) + } + repo := &CoprRepo{ + User: strings.Split(repoName, "/")[0], + Project: strings.Split(repoName, "/")[1], + } + return repo, nil +} + +func (c *CoprRepo) Name() string { + return strings.Join([]string{c.User, c.Project}, "/") +} + +func (c *CoprRepo) RepoUrl() string { + base, err := url.Parse(CoprUrl) + if err != nil { + log.Fatal(err) + } + return base.JoinPath(c.Name()).String() +} + +func (c *CoprRepo) RemoteFileName(fs afero.Fs) string { + return strings.Join([]string{c.User, c.Project, FedoraReleaseVersion(fs)}, "-") + ".repo" +} + +func (c *CoprRepo) RepoConfigUrl(fs afero.Fs) string { + fedoraRelease := "fedora-" + FedoraReleaseVersion(fs) + base, err := url.Parse(c.RepoUrl()) + if err != nil { + log.Fatal(err) + } + repoUrl := base.JoinPath("repo", fedoraRelease, c.RemoteFileName(fs)) + return repoUrl.String() +} + +func (c *CoprRepo) DefaultLocalFileName() string { + fileName := strings.Join([]string{"_copr", CoprHost, c.User, c.Project + ".repo"}, ":") + return fileName +} + +func (c *CoprRepo) LocalFilePath() string { + return ReposDir + c.DefaultLocalFileName() +} + +func (c *CoprRepo) LocalFileExists(fs afero.Fs) bool { + _, err := fs.Stat(c.LocalFilePath()) + return !os.IsNotExist(err) +} + +func (c *CoprRepo) FindLocalFiles(fs afero.Fs) error { + files, err := afero.ReadDir(fs, ReposDir) + if err != nil { + return err + } + for _, file := range files { + result, err := afero.FileContainsBytes(fs, ReposDir+file.Name(), []byte(c.Name())) + if err != nil { + return err + } + if result { + c.LocalFiles = append(c.LocalFiles, file.Name()) + } + } + return nil +} + +func (c *CoprRepo) PruneDuplicates(fs afero.Fs, out io.Writer) (bool, error) { + if len(c.LocalFiles) == 0 { + fmt.Fprintf(out, "Repository %s is not installed.", c.Name()) + } else if len(c.LocalFiles) > 1 { + if _, err := fs.Open(ReposDir + c.DefaultLocalFileName()); err != nil { + err := fs.Rename(ReposDir+c.LocalFiles[0], ReposDir+c.DefaultLocalFileName()) + if err != nil { + return false, err + } + c.LocalFiles[0] = c.DefaultLocalFileName() + } + pruneCount := 0 + for _, fileName := range c.LocalFiles { + if fileName != c.DefaultLocalFileName() { + err := fs.Remove(ReposDir + fileName) + if err != nil { + return true, err + } + pruneCount++ + //TODO remove the element from LocalFiles + } + } + if pruneCount == 1 { + fmt.Fprintf(out, "Removed 1 duplicate entry for %s.\n", c.Name()) + } else if pruneCount > 1 { + fmt.Fprintf(out, "Removed %d duplicate entries for %s.\n", pruneCount, c.Name()) + } + return true, nil + } + return false, nil +} diff --git a/internal/app/util.go b/internal/app/util.go new file mode 100644 index 0000000..d0dd4c9 --- /dev/null +++ b/internal/app/util.go @@ -0,0 +1,158 @@ +package app + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "slices" + "strings" + + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +const ( + CoprUrl string = "https://copr.fedorainfracloud.org/coprs/" + CoprHost string = "copr.fedorainfracloud.org" + ReposDir string = "/etc/yum.repos.d/" + Enabled RepoState = "enabled=1" + Disabled RepoState = "enabled=0" +) + +func FedoraReleaseVersion(fs afero.Fs) string { + // osRelease, err := ini.Load("/etc/os-release") + reader := viper.New() + reader.SetFs(fs) + reader.SetConfigName("os-release") + reader.SetConfigType("ini") + reader.AddConfigPath("/etc/") + reader.ReadInConfig() + osRelease := reader.GetString("default.version_id") + + return osRelease +} + +func SudoMessage(err error, out io.Writer) { + if errors.Is(err, fs.ErrPermission) { + fmt.Fprintf(out, "This command must be run with superuser privileges.\nError: %s\n", err) + } else { + fmt.Fprintln(out, err) + } +} + +func WriteRepoToFile(r *CoprRepo, fs afero.Fs, content []byte) error { + err := afero.WriteFile(fs, r.LocalFilePath(), content, 0644) + if err != nil { + return err + } + return nil +} + +func ToggleRepo(r *CoprRepo, fs afero.Fs, out io.Writer, desiredState RepoState) error { + repoFile := r.LocalFilePath() + contents, err := afero.ReadFile(fs, repoFile) + if err != nil { + if errors.Is(err, afero.ErrFileNotFound) { + return fmt.Errorf("repository %s/%s is not installed", r.User, r.Project) + } + return err + } + fileLines := strings.Split(string(contents), "\n") + + var statusMessage string + if desiredState == Enabled { + statusMessage = "enabled" + } else { + statusMessage = "disabled" + } + + for i, line := range fileLines { + if strings.Contains(line, "enabled=") { + if line == string(desiredState) { + fmt.Fprintf(out, "Repository %s is already %s.\n", r.Name(), statusMessage) + return nil + } else { + fileLines[i] = string(desiredState) + } + } + } + output := strings.Join(fileLines, "\n") + err = WriteRepoToFile(r, fs, []byte(output)) + if err != nil { + return err + } + fmt.Fprintf(out, "Repository %s/%s %s.\n", r.User, r.Project, statusMessage) + return nil +} + +func AddRepo(r *CoprRepo, fs afero.Fs, out io.Writer) error { + resp, err := http.Get(r.RepoConfigUrl(fs)) + if err != nil { + return err + } + output, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + err = WriteRepoToFile(r, fs, []byte(output)) + if err != nil { + return err + } + fmt.Fprintf(out, "Repository %s/%s added.\n", r.User, r.Project) + return nil +} + +func DeleteRepo(r *CoprRepo, fs afero.Fs, out io.Writer) error { + if r.LocalFileExists(fs) { + err := fs.Remove(r.LocalFilePath()) + if err != nil { + return err + } + fmt.Fprintf(out, "Repository %s/%s deleted.\n", r.User, r.Project) + } else { + fmt.Fprintf(out, "Repository %s/%s does not exist locally. Nothing to delete.\n", r.User, r.Project) + } + return nil +} + +func GetAllRepos(fs afero.Fs, out io.Writer) ([]*CoprRepo, error) { + files, err := afero.ReadDir(fs, ReposDir) + if err != nil { + return nil, err + } + var reposStrings []string + var repos []*CoprRepo + for _, file := range files { + if !file.IsDir() { + ioFile, err := fs.Open(ReposDir + file.Name()) + + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(ioFile) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "[copr:copr") { + t := strings.Split(strings.Trim(scanner.Text(), "[]"), ":") + repoName := t[len(t)-2] + "/" + t[len(t)-1] + if !slices.Contains(reposStrings, repoName) { + r, err := NewCoprRepo(repoName) + if err != nil { + return nil, err + } + repos = append(repos, r) + reposStrings = append(reposStrings, repoName) + } + break + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintln(out, "Issue reading repo files: ", err) + } + } + } + return repos, nil +} diff --git a/internal/testutil/test-utils.go b/internal/testutil/test-utils.go new file mode 100644 index 0000000..08c075e --- /dev/null +++ b/internal/testutil/test-utils.go @@ -0,0 +1,21 @@ +package testutil + +import ( + "github.com/spf13/afero" + "github.com/trgeiger/copr-tool/internal/app" +) + +func AssembleTestFs(repoFiles [][]string, otherFiles [][]string) afero.Fs { + fs := afero.NewMemMapFs() + fs.Mkdir("/etc/yum.repos.d/", 0755) + localFs := afero.NewOsFs() + for _, file := range repoFiles { + testFile, _ := afero.ReadFile(localFs, "./test/"+file[1]+"/"+file[0]) + _ = afero.WriteFile(fs, app.ReposDir+file[0], testFile, 0755) + } + for _, file := range otherFiles { + testFile, _ := afero.ReadFile(localFs, "./test/"+file[2]+"/"+file[0]) + _ = afero.WriteFile(fs, file[1]+file[0], testFile, 0755) + } + return fs +} diff --git a/main.go b/main.go index 05f600f..66d37d2 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,14 @@ /* Copyright © 2024 NAME HERE - */ package main -import "github.com/trgeiger/copr-tool/cmd" +import ( + "github.com/spf13/afero" + "github.com/trgeiger/copr-tool/cmd" +) func main() { - cmd.Execute() + fs := afero.NewOsFs() + cmd.Execute(fs) }