-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial work on adding a cli framework
create command recreate cluster added unit tests and fixed error handling fix up go-gc errors fixed minor error with clusterName reported during inventory checks
- Loading branch information
1 parent
4b9b411
commit 2952966
Showing
13 changed files
with
1,013 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# seeder-cli | ||
seeder plugin allows automation of some routine tasks with seeder. | ||
It can be used as a kubectl plugin by placing it in your path, and renaming the binary as kubectl-seeder or a standalone binary | ||
|
||
Currently supported sub commands are: | ||
* gen-kubeconfig: will generate an admin kubeconfig for a harvester cluster provisioned via seeder | ||
* create-cluster: will create a new cluster object with some basic options | ||
* recreate-cluster: will delete and re-create the cluster and patch the version if one is supplied | ||
|
||
## gen-kubeconfig | ||
```shell | ||
Usage: | ||
seeder gen-kubeconfig $CLUSTER_NAME [flags] | ||
|
||
Flags: | ||
-h, --help help for gen-kubeconfig | ||
-p, --path string path to place generated harvester cluster kubeconfig | ||
|
||
Global Flags: | ||
-d, --debug enable debug logging | ||
-n, --namespace string namespace | ||
``` | ||
|
||
## create-cluster | ||
```shell | ||
Usage: | ||
seeder create-cluster $CLUSTER_NAME [options] [flags] | ||
|
||
Flags: | ||
--address-pool string addresspool to be used for address allocation for VIP and inventory nodes | ||
--config-url string [optional] location of common harvester config that will be applied to all nodes | ||
-h, --help help for create-cluster | ||
--image-url string [optional] location where artifacts for pxe booting inventory are present | ||
--inventory strings list of inventory objects in namespace to be used for cluster | ||
--static-vip string [optional] static address for harvester cluster vip (optional). If not specified an address from addresspool will be used | ||
-v, --version string version of harvester | ||
|
||
Global Flags: | ||
-d, --debug enable debug logging | ||
-n, --namespace string namespace | ||
``` | ||
|
||
## recreate-cluster | ||
```sbell | ||
Usage: | ||
seeder recreate-cluster $CLUSTER_NAME [flags] | ||
Flags: | ||
-h, --help help for recreate-cluster | ||
-v, --version string [optional] version to use to recreate cluster | ||
Global Flags: | ||
-d, --debug enable debug logging | ||
-n, --namespace string namespace | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package main | ||
|
||
import ( | ||
"github.com/harvester/seeder/cmd/seeder/pkg/plugin" | ||
command "github.com/rancher/wrangler-cli" | ||
) | ||
|
||
func main() { | ||
command.Main(plugin.New()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
package plugin | ||
|
||
import ( | ||
"fmt" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
|
||
seederv1alpha1 "github.com/harvester/seeder/pkg/api/v1alpha1" | ||
"github.com/pkg/errors" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/types" | ||
|
||
command "github.com/rancher/wrangler-cli" | ||
"github.com/sirupsen/logrus" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type CreateCluster struct { | ||
Version string `usage:"version of harvester" short:"v"` | ||
Inventory []string `usage:"list of inventory objects in namespace to be used for cluster"` | ||
AddressPool string `usage:"addresspool to be used for address allocation for VIP and inventory nodes"` | ||
StaticVIP string `usage:"[optional] static address for harvester cluster vip (optional). If not specified an address from addresspool will be used"` | ||
ConfigURL string `usage:"[optional] location of common harvester config that will be applied to all nodes"` | ||
ImageURL string `usage:"[optional] location where artifacts for pxe booting inventory are present"` | ||
} | ||
|
||
var ( | ||
clusterName string | ||
createClusterPreflightError = errors.New("pre-flight errors detected") | ||
) | ||
|
||
func NewCreateCluster() *cobra.Command { | ||
cc := command.Command(&CreateCluster{}, cobra.Command{ | ||
Short: "create cluster", | ||
Long: `create-cluster will create a new cluster.metal.harvesterhci.io object from the flags provided. | ||
It acts as a simple wrapper around the yaml based cluster definition, and aims to be a quick start for provisioning clusters. | ||
For more advanced use cases where additional options need to be provided, please use the yaml based cluster definition method`, | ||
Use: "create-cluster $CLUSTER_NAME [options]", | ||
Args: cobra.ExactArgs(1), | ||
}) | ||
return cc | ||
} | ||
|
||
func (c *CreateCluster) Run(cmd *cobra.Command, args []string) error { | ||
logrus.Debug(args) | ||
err := c.preflightchecks(cmd, args) | ||
if err != nil { | ||
return err | ||
} | ||
cmd.Println(genHeaderMessage(fmt.Sprintf("creating new cluster %s", clusterName))) | ||
err = c.createCluster(cmd) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
cmd.Println(genHeaderMessage(fmt.Sprintf("cluster %s created", clusterName))) | ||
return nil | ||
} | ||
|
||
// Pre-Run will check if flags are set | ||
func (c *CreateCluster) Pre(cmd *cobra.Command, args []string) error { | ||
// check flags are set | ||
var err error | ||
requiredFlags := []string{"address-pool", "inventory", "version"} | ||
for _, rf := range requiredFlags { | ||
if flagErr := cmd.MarkFlagRequired(rf); flagErr != nil { | ||
err = errors.Wrap(err, flagErr.Error()) | ||
} | ||
} | ||
|
||
return err | ||
} | ||
|
||
func (c *CreateCluster) preflightchecks(cmd *cobra.Command, args []string) error { | ||
type preFlightFuncs func(*cobra.Command) (bool, error) | ||
cmd.Println(genHeaderMessage("running pre-flight checks for create-cluster")) | ||
clusterName = args[0] | ||
checkList := []preFlightFuncs{ | ||
c.inventoryExists, | ||
c.addressPoolExists, | ||
c.clusterExists, | ||
} | ||
|
||
var preFlightFailures bool | ||
for _, v := range checkList { | ||
ok, err := v(cmd) | ||
if err != nil { | ||
return err | ||
} | ||
preFlightFailures = preFlightFailures || ok | ||
} | ||
|
||
if preFlightFailures { | ||
cmd.PrintErrln(genFailMessage("one or more pre-flight checks failed")) | ||
return createClusterPreflightError | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *CreateCluster) inventoryExists(cmd *cobra.Command) (bool, error) { | ||
var preCheckFailed bool | ||
for _, i := range c.Inventory { | ||
invObj := &seederv1alpha1.Inventory{} | ||
err := mgmtClient.Get(cmd.Context(), types.NamespacedName{Namespace: namespace, Name: i}, invObj) | ||
if err != nil { | ||
if apierrors.IsNotFound(err) { | ||
preCheckFailed = true | ||
cmd.Println(genFailMessage(fmt.Sprintf("🖥 unable to find inventory %s in namespace %s", i, namespace))) | ||
continue | ||
} else { | ||
return false, err | ||
} | ||
} | ||
|
||
if invObj.Status.Cluster.Name != "" { | ||
preCheckFailed = true | ||
cmd.Println(genFailMessage(fmt.Sprintf("🖥 already allocated to cluster %s in namespace %s", invObj.Status.Cluster.Name, | ||
namespace))) | ||
continue | ||
} | ||
|
||
if invObj.Status.Status != seederv1alpha1.InventoryReady { | ||
preCheckFailed = true | ||
cmd.Println(genFailMessage(fmt.Sprintf("🖥 inventory %s in namespace %s is not ready for allocation", i, | ||
namespace))) | ||
continue | ||
} | ||
|
||
cmd.Println(genPassMessage(fmt.Sprintf("🖥 inventory %s in namespace %s is ready", i, | ||
namespace))) | ||
|
||
} | ||
|
||
return preCheckFailed, nil | ||
} | ||
|
||
func (c *CreateCluster) addressPoolExists(cmd *cobra.Command) (bool, error) { | ||
addObj := &seederv1alpha1.AddressPool{} | ||
err := mgmtClient.Get(cmd.Context(), types.NamespacedName{Namespace: namespace, Name: c.AddressPool}, addObj) | ||
if err != nil { | ||
if apierrors.IsNotFound(err) { | ||
cmd.Println(genFailMessage(fmt.Sprintf("🖥 unable to find addresspool %s in namespace %s", c.AddressPool, namespace))) | ||
return true, nil | ||
} else { | ||
return false, err | ||
} | ||
} | ||
|
||
if addObj.Status.Status != seederv1alpha1.PoolReady { | ||
cmd.Println(genFailMessage(fmt.Sprintf("🖥 addresspool %s in namespace %s is not ready", c.AddressPool, namespace))) | ||
return true, nil | ||
} | ||
|
||
cmd.Println(genPassMessage(fmt.Sprintf("🖥 addresspool %s in namespace %s is ready", c.AddressPool, namespace))) | ||
|
||
return false, nil | ||
} | ||
|
||
func (c *CreateCluster) clusterExists(cmd *cobra.Command) (bool, error) { | ||
clusterObj := &seederv1alpha1.Cluster{} | ||
err := mgmtClient.Get(cmd.Context(), types.NamespacedName{Namespace: namespace, Name: clusterName}, clusterObj) | ||
if err != nil { | ||
if apierrors.IsNotFound(err) { | ||
cmd.Println(genPassMessage(fmt.Sprintf("🖥 no cluster %s exists in namespace %s", clusterName, namespace))) | ||
return false, nil | ||
} else { | ||
return false, err | ||
} | ||
} | ||
cmd.Println(genFailMessage(fmt.Sprintf("🖥 cluster %s already exists in namespace %s", clusterName, namespace))) | ||
return true, nil | ||
} | ||
|
||
func (c *CreateCluster) generateCluster() *seederv1alpha1.Cluster { | ||
cluster := &seederv1alpha1.Cluster{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: clusterName, | ||
Namespace: namespace, | ||
}, | ||
Spec: seederv1alpha1.ClusterSpec{ | ||
HarvesterVersion: c.Version, | ||
ClusterConfig: seederv1alpha1.ClusterConfig{ | ||
ConfigURL: c.ConfigURL, | ||
}, | ||
}, | ||
} | ||
|
||
if c.ImageURL != "" { | ||
cluster.Spec.ImageURL = c.ImageURL | ||
} | ||
|
||
var nodes []seederv1alpha1.NodeConfig | ||
for _, v := range c.Inventory { | ||
nodes = append(nodes, seederv1alpha1.NodeConfig{ | ||
InventoryReference: seederv1alpha1.ObjectReference{ | ||
Name: v, | ||
Namespace: namespace, | ||
}, | ||
AddressPoolReference: seederv1alpha1.ObjectReference{ | ||
Name: c.AddressPool, | ||
Namespace: namespace, | ||
}, | ||
}) | ||
} | ||
|
||
vipConfig := seederv1alpha1.VIPConfig{ | ||
AddressPoolReference: seederv1alpha1.ObjectReference{ | ||
Name: c.AddressPool, | ||
Namespace: namespace, | ||
}, | ||
} | ||
|
||
if c.StaticVIP != "" { | ||
vipConfig.StaticAddress = c.StaticVIP | ||
} | ||
|
||
cluster.Spec.Nodes = nodes | ||
cluster.Spec.VIPConfig = vipConfig | ||
return cluster | ||
} | ||
|
||
func (c *CreateCluster) createCluster(cmd *cobra.Command) error { | ||
|
||
cluster := c.generateCluster() | ||
err := mgmtClient.Create(cmd.Context(), cluster) | ||
if err != nil { | ||
return err | ||
} | ||
cmd.Println(genPassMessage(fmt.Sprintf("cluster %s submitted successfully", clusterName))) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package plugin | ||
|
||
import ( | ||
"testing" | ||
|
||
seederv1alpha1 "github.com/harvester/seeder/pkg/api/v1alpha1" | ||
"k8s.io/apimachinery/pkg/types" | ||
|
||
"github.com/harvester/seeder/pkg/mock" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
func Test_CommandCreateClusterPass(t *testing.T) { | ||
var err error | ||
cmd := &cobra.Command{} | ||
inv := []string{"inventory-1"} | ||
addPool := "mock-pool" | ||
namespace = "default" | ||
imageURL := "http://localhost/iso" | ||
|
||
c := &CreateCluster{ | ||
Version: "v1.0.3", | ||
Inventory: inv, | ||
AddressPool: addPool, | ||
ImageURL: imageURL, | ||
} | ||
assert := require.New(t) | ||
mgmtClient, err = mock.GenerateFakeClient() | ||
assert.NoError(err, "expected no error during generation of mock client") | ||
err = c.preflightchecks(cmd, []string{"mock-cluster"}) | ||
assert.NoError(err, "expected no error during preflightchecks") | ||
|
||
err = c.createCluster(cmd) | ||
assert.NoError(err, "expected no error during cluster creation") | ||
clusterObj := &seederv1alpha1.Cluster{} | ||
err = mgmtClient.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, clusterObj) | ||
assert.NoError(err, "expect no error looking up cluster") | ||
assert.Equal(addPool, clusterObj.Spec.VIPConfig.AddressPoolReference.Name, "expected vip addresspools to match") | ||
assert.Len(clusterObj.Spec.Nodes, 1, "expected to find one node") | ||
assert.Equal(addPool, clusterObj.Spec.Nodes[0].AddressPoolReference.Name, "expected node address pools to match") | ||
} | ||
|
||
func Test_CommandCreateClusterMissingInventory(t *testing.T) { | ||
var err error | ||
cmd := &cobra.Command{} | ||
inv := []string{"inventory-3"} | ||
addPool := "mock-pool" | ||
namespace = "default" | ||
imageURL := "http://localhost/iso" | ||
|
||
c := &CreateCluster{ | ||
Version: "v1.0.3", | ||
Inventory: inv, | ||
AddressPool: addPool, | ||
ImageURL: imageURL, | ||
} | ||
assert := require.New(t) | ||
mgmtClient, err = mock.GenerateFakeClient() | ||
assert.NoError(err, "expected no error during generation of mock client") | ||
err = c.preflightchecks(cmd, []string{"mock-cluster"}) | ||
assert.Error(err, "expected no error during preflightchecks") | ||
assert.ErrorIs(err, createClusterPreflightError) | ||
} |
Oops, something went wrong.