Skip to content

Commit

Permalink
Merge pull request #105 from replicatedhq/installer-ls
Browse files Browse the repository at this point in the history
Adding "installer ls" and "installer create" commands to CLI
  • Loading branch information
dexhorthy authored Mar 30, 2020
2 parents af92818 + b49e711 commit 51348e6
Show file tree
Hide file tree
Showing 21 changed files with 541 additions and 19 deletions.
2 changes: 1 addition & 1 deletion cli/cmd/collector_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (r *runners) collectorInspect(cmd *cobra.Command, args []string) error {
collector, err := r.api.GetCollector(r.appID, id)
if err != nil {
if err == platformclient.ErrNotFound {
return fmt.Errorf("No such collector %d", id)
return fmt.Errorf("no such collector %s", id)
}
return err
}
Expand Down
16 changes: 16 additions & 0 deletions cli/cmd/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cmd

import (
"github.com/spf13/cobra"
)

func (r *runners) InitInstallerCommand(parent *cobra.Command) *cobra.Command {
installerCommand := &cobra.Command{
Use: "installer",
Short: "Manage Kubernetes installers",
Long: `The installers command allows vendors to create, display, modify and promote kurl.sh specs for managing the installation of Kubernetes.`,
}
parent.AddCommand(installerCommand)

return installerCommand
}
106 changes: 106 additions & 0 deletions cli/cmd/installer_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"io/ioutil"
)

func (r *runners) InitInstallerCreate(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "create",
Short: "Create a new installer spec",
Long: `Create a new installer spec by providing YAML configuration for a https://kurl.sh cluster.`,
}

parent.AddCommand(cmd)

cmd.Flags().StringVar(&r.args.createInstallerYaml, "yaml", "", "The YAML config for this installer. Use '-' to read from stdin. Cannot be used with the `yaml-file` falg.")
cmd.Flags().StringVar(&r.args.createInstallerYamlFile, "yaml-file", "", "The file name with YAML config for this installer. Cannot be used with the `yaml` flag.")
cmd.Flags().StringVar(&r.args.createInstallerPromote, "promote", "", "Channel name or id to promote this installer to")
cmd.Flags().BoolVar(&r.args.createInstallerPromoteEnsureChannel, "ensure-channel", false, "When used with --promote <channel>, will create the channel if it doesn't exist")

cmd.RunE = r.installerCreate
}

func (r *runners) installerCreate(_ *cobra.Command, _ []string) error {
if r.appType != "kots" {
return errors.Errorf("Installer specs are only supported for KOTS applications, app %q has type %q", r.appID, r.appType)
}

if r.args.createInstallerYaml == "" &&
r.args.createInstallerYamlFile == "" {
return errors.New("one of --yaml, --yaml-file is required")
}

// can't ensure a channel if you didn't pass one
if r.args.createInstallerPromoteEnsureChannel && r.args.createInstallerPromote == "" {
return errors.New("cannot use the flag --ensure-channel without also using --promote <channel> ")
}

if r.args.createInstallerYaml != "" && r.args.createInstallerYamlFile != "" {
return errors.New("only one of --yaml or --yaml-file may be specified")
}

if r.args.createInstallerYaml == "-" {
bytes, err := ioutil.ReadAll(r.stdin)
if err != nil {
return errors.Wrap(err, "read from stdin")
}
r.args.createInstallerYaml = string(bytes)
}

if r.args.createInstallerYamlFile != "" {
bytes, err := ioutil.ReadFile(r.args.createInstallerYamlFile)
if err != nil {
return errors.Wrap(err, "read file yaml")
}
r.args.createInstallerYaml = string(bytes)
}

// if the --promote param was used make sure it identifies exactly one
// channel before proceeding
var promoteChanID string
if r.args.createInstallerPromote != "" {
var err error
promoteChanID, err = r.getOrCreateChannelForPromotion(
r.args.createInstallerPromote,
r.args.createInstallerPromoteEnsureChannel,
)
if err != nil {
return errors.Wrapf(err, "get or create channel %q for promotion", promoteChanID)
}
}

installerSpec, err := r.api.CreateInstaller(r.appID, r.appType, r.args.createInstallerYaml)
if err != nil {
return errors.Wrap(err, "create installer")
}

if _, err := fmt.Fprintf(r.w, "SEQUENCE: %d\n", installerSpec.Sequence); err != nil {
return errors.Wrap(err, "print sequence to r.w")
}
r.w.Flush()

// don't send a version label as its not really meaningful
noVersionLabel := ""

if promoteChanID != "" {
if err := r.api.PromoteInstaller(
r.appID,
r.appType,
installerSpec.Sequence,
promoteChanID,
noVersionLabel,
); err != nil {
return errors.Wrap(err, "promote installer")
}

// ignore error since operation was successful
fmt.Fprintf(r.w, "Channel %s successfully set to release %d\n", promoteChanID, installerSpec.Sequence)
r.w.Flush()
}

return nil
}
26 changes: 26 additions & 0 deletions cli/cmd/installer_ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cmd

import (
"github.com/replicatedhq/replicated/cli/print"
"github.com/spf13/cobra"
)

func (r *runners) InitInstallerList(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "ls",
Short: "List an app's Kubernetes Installers",
Long: "List an app's https://kurl.sh Kubernetes Installers",
}

parent.AddCommand(cmd)
cmd.RunE = r.installerList
}

func (r *runners) installerList(_ *cobra.Command, _ []string) error {
installers, err := r.api.ListInstallers(r.appID, r.appType)
if err != nil {
return err
}

return print.Installers(r.w, installers)
}
16 changes: 10 additions & 6 deletions cli/cmd/release_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (r *runners) InitReleaseCreate(parent *cobra.Command) {
cmd.RunE = r.releaseCreate
}

func (r *runners) releaseCreate(cmd *cobra.Command, args []string) error {
func (r *runners) releaseCreate(_ *cobra.Command, _ []string) error {
if r.args.createReleaseYaml == "" &&
r.args.createReleaseYamlFile == "" &&
r.args.createReleaseYamlDir == "" {
Expand Down Expand Up @@ -92,7 +92,10 @@ func (r *runners) releaseCreate(cmd *cobra.Command, args []string) error {
var promoteChanID string
if r.args.createReleasePromote != "" {
var err error
promoteChanID, err = r.getOrCreateChannelForPromotion(r.args.createReleasePromoteEnsureChannel)
promoteChanID, err = r.getOrCreateChannelForPromotion(
r.args.createReleasePromote,
r.args.createReleasePromoteEnsureChannel,
)
if err != nil {
return errors.Wrapf(err, "get or create channel %q for promotion", promoteChanID)
}
Expand Down Expand Up @@ -129,18 +132,19 @@ func (r *runners) releaseCreate(cmd *cobra.Command, args []string) error {
return nil
}

func (r *runners) getOrCreateChannelForPromotion(createIfAbsent bool) (string, error) {
func (r *runners) getOrCreateChannelForPromotion(channelName string, createIfAbsent bool) (string, error) {

description := "" // todo: do we want a flag for the desired channel description

channel, err := r.api.GetChannelByName(
r.appID,
r.appType,
r.args.createReleasePromote,
channelName,
description,
createIfAbsent,
); if err != nil {
return "", errors.Wrapf(err, "get-or-create channel %q", r.args.createReleasePromote)
)
if err != nil {
return "", errors.Wrapf(err, "get-or-create channel %q", channelName)
}

return channel.ID, nil
Expand Down
16 changes: 14 additions & 2 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var appSlugOrID string
var apiToken string
var platformOrigin = "https://api.replicated.com/vendor"
var graphqlOrigin = "https://g.replicated.com/graphql"
var kurlDotSHOrigin = "https://kurl.sh"

func init() {
originFromEnv := os.Getenv("REPLICATED_API_ORIGIN")
Expand Down Expand Up @@ -141,6 +142,10 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
runCmds.InitCustomersLSCommand(customersCmd)
runCmds.InitCustomersCreateCommand(customersCmd)

installerCmd := runCmds.InitInstallerCommand(runCmds.rootCmd)
runCmds.InitInstallerCreate(installerCmd)
runCmds.InitInstallerList(installerCmd)

runCmds.rootCmd.SetUsageTemplate(rootCmdUsageTmpl)

prerunCommand := func(cmd *cobra.Command, args []string) error {
Expand All @@ -150,16 +155,22 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
return errors.New("Please provide your API token")
}
}

// allow override
if os.Getenv("KURL_SH_ORIGIN") != "" {
kurlDotSHOrigin = os.Getenv("KURL_SH_ORIGIN")
}

platformAPI := platformclient.NewHTTPClient(platformOrigin, apiToken)
runCmds.platformAPI = platformAPI

shipAPI := shipclient.NewGraphQLClient(graphqlOrigin, apiToken)
runCmds.shipAPI = shipAPI

kotsAPI := kotsclient.NewGraphQLClient(graphqlOrigin, apiToken)
kotsAPI := kotsclient.NewGraphQLClient(graphqlOrigin, apiToken, kurlDotSHOrigin)
runCmds.kotsAPI = kotsAPI

commonAPI := client.NewClient(platformOrigin, graphqlOrigin, apiToken)
commonAPI := client.NewClient(platformOrigin, graphqlOrigin, apiToken, kurlDotSHOrigin)
runCmds.api = commonAPI

if appSlugOrID == "" {
Expand Down Expand Up @@ -200,6 +211,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
collectorsCmd.PersistentPreRunE = prerunCommand
entitlementsCmd.PersistentPreRunE = prerunCommand
customersCmd.PersistentPreRunE = prerunCommand
installerCmd.PersistentPreRunE = prerunCommand

runCmds.rootCmd.AddCommand(Version())

Expand Down
7 changes: 6 additions & 1 deletion cli/cmd/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type runnerArgs struct {
createReleasePromoteVersion string
createReleasePromoteEnsureChannel bool
lintReleaseYamlDir string
lintReleaseFailOn string
lintReleaseFailOn string
releaseOptional bool
releaseNotes string
releaseVersion string
Expand All @@ -82,4 +82,9 @@ type runnerArgs struct {
customerCreateChannel string
customerCreateEnsureChannel bool
customerCreateExpiryDuration time.Duration

createInstallerYaml string
createInstallerYamlFile string
createInstallerPromote string
createInstallerPromoteEnsureChannel bool
}
40 changes: 40 additions & 0 deletions cli/print/installers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package print

import (
"github.com/replicatedhq/replicated/pkg/types"
"strings"
"text/tabwriter"
"text/template"
)

var installersTmplSrc = `SEQUENCE CREATED ACTIVE_CHANNELS
{{ range . -}}
{{ .Sequence }} {{ time .CreatedAt.Time }} {{ .ActiveChannels }}
{{ end }}`

var installersTmpl = template.Must(template.New("Installers").Funcs(funcs).Parse(installersTmplSrc))

func Installers(w *tabwriter.Writer, appReleases []types.InstallerSpec) error {
rs := make([]map[string]interface{}, len(appReleases))

for i, r := range appReleases {
// join active channel names like "Stable,Unstable"
activeChans := make([]string, len(r.ActiveChannels))
for j, activeChan := range r.ActiveChannels {
activeChans[j] = activeChan.Name
}
activeChansField := strings.Join(activeChans, ",")

rs[i] = map[string]interface{}{
"Sequence": r.Sequence,
"CreatedAt": r.CreatedAt,
"ActiveChannels": activeChansField,
}
}

if err := installersTmpl.Execute(w, rs); err != nil {
return err
}

return w.Flush()
}
4 changes: 4 additions & 0 deletions cli/test/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package test

import (
"os"
"testing"

. "github.com/onsi/ginkgo"
)

func TestCLI(t *testing.T) {
if os.Getenv("SKIP_INTEGRATION_TESTING") != "" {
return
}
RunSpecs(t, "CLI Suite")
}

Expand Down
5 changes: 2 additions & 3 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ type Client struct {
KotsClient *kotsclient.GraphQLClient
}

func NewClient(platformOrigin string, graphqlOrigin string, apiToken string) Client {
func NewClient(platformOrigin string, graphqlOrigin string, apiToken string, kurlOrigin string) Client {
client := Client{
PlatformClient: platformclient.NewHTTPClient(platformOrigin, apiToken),
ShipClient: shipclient.NewGraphQLClient(graphqlOrigin, apiToken),
KotsClient: kotsclient.NewGraphQLClient(graphqlOrigin, apiToken),
KotsClient: kotsclient.NewGraphQLClient(graphqlOrigin, apiToken, kurlOrigin),
}

return client
Expand All @@ -40,4 +40,3 @@ func (c *Client) GetAppType(appID string) (string, error) {

return "", err
}

45 changes: 45 additions & 0 deletions client/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package client

import (
"github.com/pkg/errors"
"github.com/replicatedhq/replicated/pkg/types"
)

func (c *Client) CreateInstaller(appId string, appType string, yaml string) (*types.InstallerSpec, error) {
if appType == "platform" {
return nil, errors.Errorf("Kubernetes Installers are not supported for platform applications")
} else if appType == "ship" {
return nil, errors.Errorf("Kubernetes Installers are not supported for platfrom applications")
} else if appType == "kots" {
return c.KotsClient.CreateInstaller(appId, yaml)
}

return nil, errors.New("unknown app type")
}

func (c *Client) ListInstallers(appId string, appType string) ([]types.InstallerSpec, error) {

if appType == "platform" {
return nil, errors.Errorf("Kubernetes Installers are not supported for platform applications")
} else if appType == "ship" {
return nil, errors.Errorf("Kubernetes Installers are not supported for platform applications")
} else if appType == "kots" {
return c.KotsClient.ListInstallers(appId)
}

return nil, errors.New("unknown app type")

}

func (c *Client) PromoteInstaller(appId string, appType string, sequence int64, channelID string, versionLabel string) error {
if appType == "platform" {
return errors.Errorf("Kubernetes Installers are not supported for platform applications")
} else if appType == "ship" {
return errors.Errorf("Kubernetes Installers are not supported for ship applications")
} else if appType == "kots" {
return c.KotsClient.PromoteInstaller(appId, sequence, channelID, versionLabel)
}

return errors.New("unknown app type")

}
Loading

0 comments on commit 51348e6

Please sign in to comment.