Skip to content

Commit

Permalink
refactor: extension (#328)
Browse files Browse the repository at this point in the history
* refactor: move extension to be under ext package as it is specified for outside communication

* feat: add github provider for extension

* feat: add default manifester

* refactor: remove install from client and add a dedicated asset installer

* refactor: unexport default manifester

* refactor: add provider name in the extension path

* feat: add default installer

* refactor: change installer to use afero for better abstraction

* refactor: update manifester to use afero for better abstraction

* feat: add manager and factory

* feat: add unit test for factory

* docs: add comment for documentation

* refactor: extract out errors

* fix: bug caused by empty tag and wrong request type

* test: add unit test for flush

* test: add test when the release is either draft or pre-release

* test: add validation if extension is already installed

* refactor: restructure extension mechanism to simplify logical interaction among components

* chore: update to fix lint issue

* fix: error message to also show the targetted version

* refactor: extract manifest modification to separate function

* fix: bug not found tag being not handled

* refactor: reformat error output to simplify for the user

* refactor: releases to use list instead of map for ease of manifest reading

* refactor: rename metadata to remote metadata to avoid confusion with repo metadata and to express more intent

* feat: allow tag latest to be used

* refactor: install method to simplify for easier readability

* test: handle case where command name is already used by other project

* refactor: reduce unnecessary implementation to simplify

* refactor: move manager install functionalities to a separate file

* test: add unit test when error encountered during updating manifest

* feat: add activate in manager to allow tag specific activation of extension

* refactor: move out api from project to the release

* feat: add rename functionality in extension manager

* feat: divide api path to current and upgrade api

* feat: add upgrade functionality on extention

* refactor: manager to better simplify the main methods

* refactor: error formatting to allow simplification or verbosity for the user

* refactor: rename GetRelease to DownloadRelease to express more intent

* refactor: rename installer to asset operator for better clarity

* feat: add uninstall functionality on manager

* refactor: update implementation to express more intent and fix bugs

* test: add unit test for uninstall

* feat: add reserved command names to avoid conflict in commands

* feat: add run method for asset operator

* feat: add run functionality

* test: add unit test for manifester and asset operator

* refactor: move out package to extension and add command

* doc: rename ext.go to doc.go

* docs: update extension documentation

* feat: add describe sub command for extension

* feat: add confirmation on uninstallation

* refactor: move out manager implementation to its own package

* test: add unit test for manager

* test: update unit test to cover more states

* refactor: move out context from struct to function

* refactor: remove http doer to instead use native http client

* refactor: fix linter issue

* refactor: extract upgrading manifest to its own function

* feat: add clean command to clean all extensions from local
  • Loading branch information
irainia authored Jun 2, 2022
1 parent 54f0784 commit 453568a
Show file tree
Hide file tree
Showing 60 changed files with 5,948 additions and 881 deletions.
54 changes: 54 additions & 0 deletions cmd/extension/activate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package extension

import (
"errors"
"fmt"

"github.com/odpf/salt/log"
"github.com/spf13/cobra"

"github.com/odpf/optimus/extension/model"
)

type activateCommand struct {
logger log.Logger

project *model.RepositoryProject
reservedCommandNames []string
}

func newActivateCommand(logger log.Logger, project *model.RepositoryProject, reservedCommandNames []string) *cobra.Command {
activate := &activateCommand{
logger: logger,
project: project,
reservedCommandNames: reservedCommandNames,
}

cmd := &cobra.Command{
Use: "activate TAG",
Short: "activate is a sub command to allow user to activate an installed tag",
RunE: activate.RunE,
}
return cmd
}

func (a *activateCommand) RunE(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("one argument for TAG should be specified")
}
tagName := args[0]
verbose, _ := cmd.Flags().GetBool("verbose")

manager, err := getExtensionManager(verbose, a.reservedCommandNames...)
if err != nil {
return err
}

a.logger.Info(fmt.Sprintf("Activating tag [%s] ...", tagName))
if err := manager.Activate(a.project.CommandName, tagName); err != nil {
a.logger.Error("... finished with error")
return err
}
a.logger.Info("... finished successfully")
return nil
}
41 changes: 41 additions & 0 deletions cmd/extension/clean.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package extension

import (
"github.com/odpf/salt/log"
"github.com/spf13/cobra"

"github.com/odpf/optimus/cmd/survey"
"github.com/odpf/optimus/extension"
)

type cleanCommand struct {
logger log.Logger
survey *survey.ExtensionSurvey
}

func newCleanCommand(logger log.Logger) *cobra.Command {
clean := &cleanCommand{
logger: logger,
survey: survey.NewExtensionSurvey(),
}
cmd := &cobra.Command{
Use: "clean",
Short: "clean all extension and its manifest from local",
Long: "this command can be used in case manifest is corrupted",
RunE: clean.RunE,
}
return cmd
}

func (c *cleanCommand) RunE(cmd *cobra.Command, _ []string) error {
verbose, _ := cmd.Flags().GetBool("verbose")
confirmed, err := c.survey.AskConfirmClean()
if err != nil {
return err
}
if !confirmed {
c.logger.Info("Aborted clean process ...")
return nil
}
return extension.Clean(verbose)
}
84 changes: 84 additions & 0 deletions cmd/extension/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package extension

import (
"bytes"
"fmt"

"github.com/odpf/salt/log"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"

"github.com/odpf/optimus/extension/model"
)

type describeCommand struct {
logger log.Logger
project *model.RepositoryProject
}

func newDescribeCommand(logger log.Logger, project *model.RepositoryProject) *cobra.Command {
describe := &describeCommand{
logger: logger,
project: project,
}

cmd := &cobra.Command{
Use: "describe",
Short: "describe is a sub command to allow user to describe extension",
RunE: describe.RunE,
}
return cmd
}

func (d *describeCommand) RunE(cmd *cobra.Command, _ []string) error {
verbose, _ := cmd.Flags().GetBool("verbose")

header := d.buildHeader()
var table string
if verbose {
table = d.buildVerboseTable()
} else {
table = d.buildSimpleTable()
}
content := header + "\nRelease:\n" + table
d.logger.Info(content)
return nil
}

func (d *describeCommand) buildHeader() string {
var output string
output += fmt.Sprintf("Onwer name: %s\n", d.project.Owner.Name)
output += fmt.Sprintf("Project name: %s\n", d.project.Name)
output += fmt.Sprintf("Command name: %s\n", d.project.CommandName)
return output
}

func (d *describeCommand) buildVerboseTable() string {
buff := &bytes.Buffer{}
table := tablewriter.NewWriter(buff)
table.SetHeader([]string{"tag", "active", "current api path", "upgrade api path"})
for _, release := range d.project.Releases {
var active string
if release.TagName == d.project.ActiveTagName {
active = "true"
}
table.Append([]string{release.TagName, active, release.CurrentAPIPath, release.UpgradeAPIPath})
}
table.Render()
return buff.String()
}

func (d *describeCommand) buildSimpleTable() string {
buff := &bytes.Buffer{}
table := tablewriter.NewWriter(buff)
table.SetHeader([]string{"tag", "active"})
for _, release := range d.project.Releases {
var active string
if release.TagName == d.project.ActiveTagName {
active = "true"
}
table.Append([]string{release.TagName, active})
}
table.Render()
return buff.String()
}
114 changes: 62 additions & 52 deletions cmd/extension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,91 @@ package extension

import (
"fmt"
"net/http"
"os"

"github.com/google/go-github/github"
"github.com/odpf/salt/log"
"github.com/spf13/cobra"

"github.com/odpf/optimus/cmd/logger"
"github.com/odpf/optimus/extension"
"github.com/odpf/optimus/extension/model"
)

// UpdateWithExtension updates input command with the available extensions
func UpdateWithExtension(cmd *cobra.Command) {
httpClient := &http.Client{}
githubClient := github.NewClient(nil)
logger := logger.NewDefaultLogger()
reservedCommandNames := getReservedCommandNames(cmd)

extensionDir, err := extension.GetDefaultDir()
if err != nil {
panic(err)
}
manifest, err := extension.LoadManifest(extensionDir)
if err != nil {
panic(err)
cmd.AddCommand(extensionCommand(logger, reservedCommandNames))

extensionExecCommands := generateRunCommands(reservedCommandNames)
for _, c := range extensionExecCommands {
cmd.AddCommand(c)
}
}

reservedCommands := getUsedCommands(cmd)
func getReservedCommandNames(cmd *cobra.Command) []string {
custom := []string{"optimus", "extension", "install", "clean"}
var output []string
for _, c := range cmd.Commands() {
output = append(output, c.Name())
}
return append(output, custom...)
}

ext, err := extension.NewExtension(
manifest,
githubClient.Repositories, httpClient,
extensionDir,
reservedCommands...,
)
if err != nil {
panic(err)
func extensionCommand(logger log.Logger, reservedCommandNames []string) *cobra.Command {
cmd := &cobra.Command{
Use: "extension SUBCOMMAND",
Short: "operate on extension",
}
cmd.PersistentFlags().BoolP("verbose", "v", false, "if true, then more message will be provided if error encountered")

cmd.AddCommand(newExtensionCommand(ext))
commands := generateCommands(manifest, ext.Run)
for _, c := range commands {
cmd.AddCommand(newInstallCommand(logger, reservedCommandNames))
cmd.AddCommand(newCleanCommand(logger))

managementCommands := generateManagementCommands(logger, reservedCommandNames)
for _, c := range managementCommands {
cmd.AddCommand(c)
}
}

func newExtensionCommand(ext *extension.Extension) *cobra.Command {
c := &cobra.Command{
Use: "extension SUBCOMMAND",
Aliases: []string{"ext"},
Short: "Operate with extension",
}
c.AddCommand(newInstallCommand(ext))
return c
return cmd
}

func generateCommands(manifest *extension.Manifest, execFn func(string, []string) error) []*cobra.Command {
output := make([]*cobra.Command, len(manifest.Metadatas))
for i, metadata := range manifest.Metadatas {
firstAlias := metadata.Aliases[0]
c := &cobra.Command{
Use: metadata.Aliases[0],
Aliases: metadata.Aliases,
Short: fmt.Sprintf("Execute %s/%s [%s] extension",
metadata.Owner, metadata.Repo, metadata.Tag,
),
RunE: func(cmd *cobra.Command, args []string) error {
return execFn(firstAlias, args)
},
func generateManagementCommands(logger log.Logger, reservedCommandNames []string) []*cobra.Command {
manifest := loadManifest()

var output []*cobra.Command
for _, owner := range manifest.RepositoryOwners {
for _, project := range owner.Projects {
cmd := &cobra.Command{
Use: project.CommandName,
Short: fmt.Sprintf("Sub-command to operate over extension [%s/%s@%s]",
owner.Name, project.Name, project.ActiveTagName,
),
}
cmd.AddCommand(newActivateCommand(logger, project, reservedCommandNames))
cmd.AddCommand(newDescribeCommand(logger, project))
cmd.AddCommand(newRenameCommand(logger, project, reservedCommandNames))
cmd.AddCommand(newUninstallCommand(logger, project, reservedCommandNames))
cmd.AddCommand(newUpgradeCommand(logger, project, reservedCommandNames))

output = append(output, cmd)
}
c.DisableFlagParsing = true
output[i] = c
}
return output
}

func getUsedCommands(cmd *cobra.Command) []string {
output := make([]string, len(cmd.Commands()))
for i, c := range cmd.Commands() {
output[i] = c.Name()
func loadManifest() *model.Manifest {
manifester := extension.NewDefaultManifester()
manifest, err := manifester.Load(model.ExtensionDir)
if err != nil {
panic(fmt.Errorf("error loading manifest, try running `optimus extension clean` to fix: %w", err))
}
return output
return manifest
}

func getExtensionManager(verbose bool, reservedCommandNames ...string) (*extension.Manager, error) {
manifester := extension.NewDefaultManifester()
assetOperator := extension.NewDefaultAssetOperator(os.Stdin, os.Stdout, os.Stderr)
return extension.NewManager(manifester, assetOperator, verbose, reservedCommandNames...)
}
Loading

0 comments on commit 453568a

Please sign in to comment.