Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'keto get config' functionality #99

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions bin/keto_test_e2e_exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ set +e
WORKINGDIR="${GOPATH}/src/github.com/UKHomeOffice/keto"
KETO_ASSETS_DIR=${KETO_ASSETS_DIR:-${PWD}}

function generate_kube_config() {
API_ADDR="https://$(aws cloudformation describe-stacks --stack-name keto-${CLUSTER_NAME}-elb --query "Stacks[0].Outputs[?OutputKey=='ELBDNS'].OutputValue" --output text)"
mkdir -p ~/.kube/
kubeadm alpha phase kubeconfig client-certs --client-name kubernetes-admin --organization system:masters --server ${API_ADDR} --cert-dir ${KETO_ASSETS_DIR} > ~/.kube/config
}

function cleanup() {
echo "[INFO] Attempting to delete keto cluster '${CLUSTER_NAME}'"
keto --cloud ${KETO_CLOUD_PROVIDER} delete cluster ${CLUSTER_NAME}
Expand All @@ -33,7 +27,8 @@ function run_e2e_test() {

# TODO: Modify once Keto adds capability to auto generate config using the client cli
echo "[INFO] Generating Kubernetes config"
generate_kube_config || return
mkdir -p ~/.kube
keto get config --cloud aws --cluster ${CLUSTER_NAME} --assets-dir ${KETO_ASSETS_DIR} --output-file ~/.kube/config || return

echo "[INFO] Executing kuberang test to validate cluster health"
${WORKINGDIR}/bin/kuberang.sh 180 || return
Expand Down
3 changes: 0 additions & 3 deletions bin/keto_test_e2e_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ glide install
# cfssl for cert generation
go get -u github.com/cloudflare/cfssl/cmd/...

# TODO: This can be removed when keto implements the capability to locally generate the kube config
curl -LO https://bootstrap.pypa.io/get-pip.py && python get-pip.py && pip install awscli

# Get kubectl, kubeadm, kuberang
curl -LO https://storage.googleapis.com/kubernetes-release/release/${KUBE_VERSION}/bin/linux/amd64/kubectl
chmod +x kubectl && mv kubectl /usr/local/bin/kubectl && kubectl help
Expand Down
2 changes: 2 additions & 0 deletions pkg/cloudprovider/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type Clusters interface {
GetMasterPersistentIPs(clusterName string) (map[string]string, error)
// PushAssets pushes assets to cloud provider specific implementation.
PushAssets(clusterName string, a model.Assets) error
// GetKubeAPIURL retrieves the API URL for a given cluster
GetKubeAPIURL(clusterName string) (string, error)
}

// NodePooler is an abstract interface for node pools.
Expand Down
10 changes: 5 additions & 5 deletions pkg/cloudprovider/providers/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ func (c *Cloud) DescribeCluster(name string) error {
return ErrNotImplemented
}

// getKubeAPIURL returns a full Kubernetes API URL from an ELB stack.
func (c Cloud) getKubeAPIURL(clusterName string) (string, error) {
// GetKubeAPIURL returns a full Kubernetes API URL from an ELB stack.
func (c Cloud) GetKubeAPIURL(clusterName string) (string, error) {
stack, err := c.getStack(makeELBStackName(clusterName))
if err != nil {
return "", err
Expand All @@ -202,7 +202,7 @@ func formatKubeAPIURL(host string) string {
return "https://" + strings.ToLower(host)
}

// getELBName returns an ELB name from the ELB stack for a given given cluster.
// getELBName returns an ELB name from the ELB stack for a given cluster.
func (c Cloud) getELBName(clusterName string) (string, error) {
res, err := c.getStackResources(makeELBStackName(clusterName))
if err != nil {
Expand Down Expand Up @@ -428,7 +428,7 @@ func (c *Cloud) CreateMasterPool(p model.MasterPool) error {
return err
}

kubeAPIURL, err := c.getKubeAPIURL(p.ClusterName)
kubeAPIURL, err := c.GetKubeAPIURL(p.ClusterName)
if err != nil {
return err
}
Expand Down Expand Up @@ -485,7 +485,7 @@ func (c *Cloud) CreateComputePool(p model.ComputePool) error {
if err != nil {
return err
}
kubeAPIURL, err := c.getKubeAPIURL(p.ClusterName)
kubeAPIURL, err := c.GetKubeAPIURL(p.ClusterName)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cloudprovider/providers/aws/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ func TestGetKubeAPIURL(t *testing.T) {
},
}, nil).Once()

result, err := c.getKubeAPIURL(clusterName)
result, err := c.GetKubeAPIURL(clusterName)
if err != nil {
t.Error(err)
}
Expand Down
42 changes: 42 additions & 0 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package controller
import (
"errors"
"fmt"
"os/exec"
"regexp"
"strings"

"github.com/UKHomeOffice/keto/pkg/cloudprovider"
"github.com/UKHomeOffice/keto/pkg/constants"
Expand Down Expand Up @@ -356,6 +359,45 @@ func (c *Controller) DeleteCluster(names ...string) error {
return nil
}

// GetClusterConfig retrieves the kubernetes config for a cluster
func (c *Controller) GetClusterConfig(clusterName string, assetsDir string) (string, error) {
cl, impl := c.Cloud.Clusters()
if !impl {
return "", ErrNotImplemented
}

apiURL, err := cl.GetKubeAPIURL(clusterName)
if err != nil {
return "", err
}

var validURL = regexp.MustCompile(`^https:\/\/.+$`)
if !validURL.MatchString(apiURL) {
return "", fmt.Errorf(
"unable to retrieve valid API URL for cluster name %q and cloud provider %q. URL: %q",
clusterName,
c.Cloud.ProviderName(),
apiURL,
)
}

c.Logger.Printf("Generating kubernetes config for cluster: %q", clusterName)

cmdName := "kubeadm"
var cmdOut []byte
cmdArgs := []string{
"alpha", "phase", "kubeconfig", "client-certs", "--client-name",
"kubernetes-admin", "--organization", "system:masters",
"--server", apiURL, "--cert-dir", assetsDir,
}
c.Logger.Printf("Running: %v %v", cmdName, strings.Join(cmdArgs, " "))
cmdOut, err = exec.Command(cmdName, cmdArgs...).CombinedOutput()
if err != nil {
return "", err
}
return string(cmdOut[:]), nil
}

// DeleteMasterPool deletes a master node pool.
func (c *Controller) DeleteMasterPool(clusterName string) error {
pooler, impl := c.Cloud.NodePooler()
Expand Down
107 changes: 107 additions & 0 deletions pkg/keto/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ limitations under the License.
package cmd

import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"runtime"

"github.com/UKHomeOffice/keto/pkg/constants"
"github.com/UKHomeOffice/keto/pkg/keto"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -129,16 +135,117 @@ func listClusters(cli *cli, names ...string) error {
return keto.PrintClusters(keto.GetPrinter(os.Stdout), clusters, true)
}

var getClusterConfigCmd = &cobra.Command{
Use: "config --cluster [NAME]",
Aliases: clusterConfigCmdAliases,
Short: "Get cluster kubernetes config",
Long: "Get cluster kubernetes config",
SilenceUsage: true,
PreRunE: func(c *cobra.Command, args []string) error {
return validateGetConfigPrecursors(c, args)
},
RunE: func(c *cobra.Command, args []string) error {
return getClusterConfigCmdFunc(c, args)
},
}

func validateGetConfigPrecursors(c *cobra.Command, args []string) error {
if !c.Flags().Changed("cluster") {
return fmt.Errorf("cluster name must be set: --cluster [NAME]")
}

_, err := exec.LookPath("kubeadm")
if err != nil {
var binaryDownloadLink bytes.Buffer
binaryDownloadLink.WriteString("https://storage.googleapis.com/kubernetes-release/release/")
binaryDownloadLink.WriteString(constants.DefaultKubeVersion)
binaryDownloadLink.WriteString("/bin/")
binaryDownloadLink.WriteString(runtime.GOOS + "/")
binaryDownloadLink.WriteString(runtime.GOARCH)
binaryDownloadLink.WriteString("/kubeadm")

return fmt.Errorf(`the executable 'kubeadm' was not found. Retrieve it using the following command:
curl -Lo %q /usr/local/bin/kubeadm && chmod +x /usr/local/bin/kubeadm
Note: If the 'kubeadm' binary is not built for your distribution the above link may not work.`, binaryDownloadLink.String())
}

_, err = c.Flags().GetString("cluster")
if err != nil {
return err
}

assetsDir, err := c.Flags().GetString("assets-dir")
if err != nil {
return err
}
if assetsDir == "" {
assetsDir, err = os.Getwd()
if err != nil {
return err
}
}

// Check if assets exist, create symlinks from 'kube_ca' files if necessary
certName := "ca.crt"
keyName := "ca.key"

if !fileExists(assetsDir) {
return fmt.Errorf("assets directory %q does not exist", assetsDir)
}
if !fileExists(path.Join(assetsDir, certName)) {
if !fileExists(path.Join(assetsDir, "kube_" + certName)) {
return fmt.Errorf("%q does not exist (kube ca certificate)", path.Join(assetsDir, certName))
}
os.Symlink(path.Join(assetsDir, "kube_" + certName), path.Join(assetsDir, certName))
}
if !fileExists(path.Join(assetsDir, keyName)) {
if !fileExists(path.Join(assetsDir, "kube_" + keyName)) {
return fmt.Errorf("%q does not exist (kube ca private key)", path.Join(assetsDir, keyName))
}
os.Symlink(path.Join(assetsDir, "kube_" + keyName), path.Join(assetsDir, keyName))
}

return nil
}

func getClusterConfigCmdFunc(c *cobra.Command, args []string) error {
clusterName, _ := c.Flags().GetString("cluster")
assetsDir, _ := c.Flags().GetString("assets-dir")
outputFile, _ := c.Flags().GetString("output-file")

cli, err := newCLI(c)
if err != nil {
return err
}

config, err := cli.ctrl.GetClusterConfig(clusterName, assetsDir)
if err != nil {
return err
}

return keto.PrintClusterConfig(keto.GetPrinter(os.Stdout), config, outputFile)
}

func init() {
getCmd.AddCommand(
getClusterCmd,
getMasterPoolCmd,
getComputePoolCmd,
getClusterConfigCmd,
)

// Add flags that are relevant to different subcommands.
addClusterFlag(
getMasterPoolCmd,
getComputePoolCmd,
getClusterConfigCmd,
)

addAssetsDirFlag(
getClusterConfigCmd,
)

addConfigOutputFileFlag(
getClusterConfigCmd,
)
}
14 changes: 11 additions & 3 deletions pkg/keto/cmd/keto.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ var (
}

// subcommand aliases
clusterCmdAliases = []string{"cl", "clusters"}
masterPoolCmdAliases = []string{"mp", "master", "masters", "masterpools"}
computePoolCmdAliases = []string{"cp", "compute", "computes", "computepools"}
clusterCmdAliases = []string{"cl", "clusters"}
masterPoolCmdAliases = []string{"mp", "master", "masters", "masterpools"}
computePoolCmdAliases = []string{"cp", "compute", "computes", "computepools"}
clusterConfigCmdAliases = []string{"conf", "configuration"}
)

// Execute adds all child commands to the root command sets flags appropriately.
Expand Down Expand Up @@ -260,3 +261,10 @@ func addSchedulerExtraArgsFlag(c ...*cobra.Command) {
i.Flags().String("scheduler-extra-args", "", "Kubernetes scheduler extra arguments")
}
}

// addConfigOutputFileFlag adds an output file flag
func addConfigOutputFileFlag(c ...*cobra.Command) {
for _, i := range c {
i.Flags().String("output-file", "", "Write config output to a file")
}
}
23 changes: 23 additions & 0 deletions pkg/keto/resource_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package keto
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"

Expand Down Expand Up @@ -89,6 +90,28 @@ func PrintComputePool(w *tabwriter.Writer, pools []*model.ComputePool, headers b
return w.Flush()
}

func PrintClusterConfig(w *tabwriter.Writer, config string, outputFile string) error {
if len(outputFile) < 1 {
fmt.Fprintln(w, config)
return w.Flush()
}

_, err := os.Stat(outputFile)
if !os.IsNotExist(err) {
return err
}

file, err := os.Create(outputFile)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(config)
file.Sync()
return w.Flush()
}

// formatData formats data of slices of string slices ready for tabwriter.
func formatData(data [][]string) string {
rows := []string{}
Expand Down