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

WIP: minikube sync #66

Closed
wants to merge 1 commit into from
Closed
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
WIP: minikube sync
  • Loading branch information
guumaster committed Jul 20, 2020
commit 011d62e1b7fb0519b5553cf2c72a784b6ec66ed7
8 changes: 8 additions & 0 deletions cmd/hostctl/actions/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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 @@ -168,6 +175,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 @@ -28,4 +28,13 @@ var (

// ErrInvalidProfileHeader when the profile header is invalid
ErrInvalidProfileHeader = errors.New("invalid format for profile header")

// 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