Skip to content

Commit

Permalink
WIP: minikube sync
Browse files Browse the repository at this point in the history
  • Loading branch information
guumaster committed Apr 26, 2020
1 parent f248ce1 commit b60b2f3
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 6 deletions.
8 changes: 8 additions & 0 deletions cmd/hostctl/actions/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ func registerCommands(rootCmd *cobra.Command) {
syncDockerComposeCmd.Flags().String("project-name", "", "docker compose project name")
syncDockerComposeCmd.Flags().Bool("prefix", false, "keep project name prefix from domain name")

// sync minikube
syncMinikubeCmd := newSyncMinikubeCmd(removeCmd)
syncMinikubeCmd.Flags().StringP("profile", "p", "minikube", "minikube profile to read from")
syncMinikubeCmd.Flags().Bool("all-namespaces", false, "read ingresses from all kubernetes namespace")
syncMinikubeCmd.Flags().
StringP("namespace", "n", "", "kubernetes namespace to read ingress data from")

// list
listCmd := newListCmd()

Expand All @@ -175,6 +182,7 @@ func registerCommands(rootCmd *cobra.Command) {
removeCmd.AddCommand(removeDomainsCmd)
syncCmd.AddCommand(syncDockerCmd)
syncCmd.AddCommand(syncDockerComposeCmd)
syncCmd.AddCommand(syncMinikubeCmd)

// register all commands
rootCmd.AddCommand(addCmd)
Expand Down
69 changes: 69 additions & 0 deletions cmd/hostctl/actions/sync_minikube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package actions

import (
"github.com/spf13/cobra"

"github.com/guumaster/hostctl/pkg/file"
"github.com/guumaster/hostctl/pkg/k8s/minikube"
"github.com/guumaster/hostctl/pkg/profile"
"github.com/guumaster/hostctl/pkg/types"
)

func newSyncMinikubeCmd(removeCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "minikube [profile] [flags]",
Short: "Sync a minikube profile with a hostctl profile.",
Long: `
Reads from Minikube the list of ingresses and add names and IPs to a profile in your hosts file.
`,
Args: commonCheckArgs,
PreRunE: func(cmd *cobra.Command, _ []string) error {
ns, _ := cmd.Flags().GetString("namespace")
allNs, _ := cmd.Flags().GetBool("all-namespaces")

if ns == "" && !allNs {
return types.ErrKubernetesNamespace
}
return nil
},
RunE: func(cmd *cobra.Command, profiles []string) error {
src, _ := cmd.Flags().GetString("host-file")
ns, _ := cmd.Flags().GetString("namespace")
allNs, _ := cmd.Flags().GetBool("all-namespace")

if allNs {
ns = ""
}

profileName := profiles[0]

mini, err := minikube.GetProfile(profileName)
if err != nil {
return err
}

p, err := profile.NewProfileFromMinikube(mini, ns)
if err != nil {
return err
}

h, err := file.NewFile(src)
if err != nil {
return err
}

p.Name = mini.Name
p.Status = types.Enabled

err = h.ReplaceProfile(p)
if err != nil {
return err
}

return h.Flush()
},
PostRunE: func(cmd *cobra.Command, args []string) error {
return postActionCmd(cmd, args, removeCmd, true)
},
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ require (
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.5.1
gopkg.in/yaml.v2 v2.2.2
k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d
k8s.io/client-go v0.0.0-20190918200256-06eb1244587a
)
84 changes: 82 additions & 2 deletions go.sum

Large diffs are not rendered by default.

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

import (
"os"
"path/filepath"

"github.com/guumaster/cligger"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)

// NewClientset returns a new clienset to interact with Kubernetes
func NewClientset() (*kubernetes.Clientset, error) {
kubeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config")

if fromEnv := os.Getenv("KUBECONFIG"); fromEnv != "" {
kubeConfigPath = fromEnv
cligger.Info("Using config from %s", kubeConfigPath)
}

// use the current context in kubeConfigPath
kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
return nil, cligger.Errorf("fatal error kubernetes config: %s", err)
}

// create the clientset
clientset, err := kubernetes.NewForConfig(kubeConfig)
if err != nil {
return nil, cligger.Errorf("error with kubernetes config:", err)
}

return clientset, nil
}
41 changes: 41 additions & 0 deletions pkg/k8s/ingress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package k8s

import (
"net"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

// Ingress contains information about an ingress rule
type Ingress struct {
IP net.IP
Hostname string
}

// GetIngresses returns a list of ingresses presents on a namespace
func GetIngresses(cli *kubernetes.Clientset, ns string) ([]Ingress, error) {
if ns == "" {
ns = v1.NamespaceAll
}

ing, err := cli.ExtensionsV1beta1().Ingresses(ns).List(v1.ListOptions{})
if err != nil {
return nil, err
}

var list []Ingress

for _, i := range ing.Items {
ip := i.Status.LoadBalancer.Ingress[0].IP

for _, r := range i.Spec.Rules {
list = append(list, Ingress{
IP: net.ParseIP(ip),
Hostname: r.Host,
})
}
}

return list, nil
}
98 changes: 98 additions & 0 deletions pkg/k8s/minikube/minikube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package minikube

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os/exec"
)

// Profile contains information about a minikube profile
type Profile struct {
Name string
IP string
Status Status
Driver string

IngressEnabled bool
IngressDNSEnabled bool
}

// Status represents the current state of a profile
type Status string

const (
// Running status of minikube profile
Running Status = "Running"
)

// GetProfile returns information about a minikube profile
func GetProfile(name string) (*Profile, error) {
mini, err := exec.LookPath("minikube")
if err != nil {
return nil, err
}

b := bytes.NewBufferString("")
c := exec.Command(mini, "profile", "list", "-o", "json")
c.Stdout = b

err = c.Run()
if err != nil {
return nil, err
}

data, err := ioutil.ReadAll(b)
if err != nil {
return nil, err
}

var m ProfileListResponse

_ = json.Unmarshal(data, &m)

var profile *Profile

for _, p := range m.Valid {
if p.Name == name {
profile = &Profile{
Name: p.Name,
IP: p.Config.Nodes[0].IP,
Status: Status(p.Status),
Driver: p.Config.Driver,
}
}
}

if profile == nil {
return nil, fmt.Errorf("can't find profile '%s' on minikube", name)
}

b = bytes.NewBufferString("")
c = exec.Command(mini, "addons", "list", "-o", "json", "-p", profile.Name)
c.Stdout = b

err = c.Run()
if err != nil {
return nil, err
}

data, err = ioutil.ReadAll(b)
if err != nil {
return nil, err
}

var a *AddonsResponse

_ = json.Unmarshal(data, &a)

if a == nil {
return profile, nil
}

profile.IngressDNSEnabled = a.IngressDNS.Status == "enabled"
profile.IngressEnabled = a.Ingress.Status == "enabled"

return profile, nil
}
27 changes: 27 additions & 0 deletions pkg/k8s/minikube/responses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package minikube

// ProfileListResponse represents the json response of `minikube profile list`
type ProfileListResponse struct {
Valid []struct {
Config struct {
Driver string `json:"Driver"`
Name string `json:"Name"`
Nodes []struct {
IP string `json:"IP"`
Name string `json:"Name"`
} `json:"Nodes"`
} `json:"Config"`
Name string `json:"Name"`
Status string `json:"Status"`
} `json:"valid"`
}

// AddonsResponse represents the json response of `minikube addons list`
type AddonsResponse struct {
Ingress struct {
Status string `json:"Status"`
} `json:"ingress"`
IngressDNS struct {
Status string `json:"Status"`
} `json:"ingress-dns"`
}
36 changes: 36 additions & 0 deletions pkg/profile/k8s.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package profile

import (
"github.com/guumaster/hostctl/pkg/k8s"
"github.com/guumaster/hostctl/pkg/k8s/minikube"
"github.com/guumaster/hostctl/pkg/types"
)

// NewProfileFromMinikube creates a new profile from a minikube profile and k8s namespace
func NewProfileFromMinikube(mini *minikube.Profile, ns string) (*types.Profile, error) {
if mini.Status != minikube.Running {
return nil, types.ErrMinikubeStatus
}

if !mini.IngressEnabled {
return nil, types.ErrMinikubeIngress
}

cli, err := k8s.NewClientset()
if err != nil {
return nil, err
}

list, err := k8s.GetIngresses(cli, ns)
if err != nil {
return nil, err
}

p := &types.Profile{}

for _, in := range list {
p.AddRoute(types.NewRoute(in.IP.String(), in.Hostname))
}

return p, nil
}
9 changes: 9 additions & 0 deletions pkg/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ var (
// ErrSnapConfinement when trying to read files on snap installation
ErrSnapConfinement = errors.New("can't use --from or --host-file. " +
"Snap confinement restrictions doesn't allow to read other than /etc/hosts file")

// ErrMinikubeStatus when minikube profile is not running
ErrMinikubeStatus = errors.New("minikube profile has to be running")

// ErrMinikubeIngress when minikube doesn't have ingress addon enabled
ErrMinikubeIngress = errors.New("minikube profile doesn't have ingress addon enabled")

// ErrKubernetesNamespace when no namespace is given
ErrKubernetesNamespace = errors.New("namespace parameter is required or use --all-namespaces")
)
8 changes: 4 additions & 4 deletions sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ sonar.projectKey=guumaster_hostctl
sonar.projectName=hostctl
sonar.projectVersion=1.0.0
sonar.sources=.
sonar.exclusions=**/*_test.go,**/cmd/*docker*.go,*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go
sonar.exclusions=**/*_test.go,**/cmd/*docker*.go,*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go,**/k8s*,**/minikube*
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.test.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go
sonar.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go
sonar.go.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go
sonar.test.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go,**/k8s*,**/minikube*
sonar.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go,**/k8s*,**/minikube*
sonar.go.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go,**/k8s*,**/minikube*
sonar.go.coverage.reportPaths=/github/workspace/ubuntu-latest_coverage/ubuntu-latest_coverage.out
#sonar.go.golangci-lint.reportPaths=/github/workspace/golangci-report.xml

0 comments on commit b60b2f3

Please sign in to comment.