Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
BeryJu committed Nov 7, 2020
0 parents commit 6a748b7
Show file tree
Hide file tree
Showing 21 changed files with 1,283 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go

### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

### Go Patch ###
/vendor/
/Godeps/

# End of https://www.toptal.com/developers/gitignore/api/go

bin/**
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.PHONY: build mover

build:
go build -v -o bin/korb

build-final:
GOOS=linux GOARCH=arm go build -v -o bin/korb-linux-arm
GOOS=linux GOARCH=arm64 go build -v -o bin/korb-linux-arm64
GOOS=linux GOARCH=amd64 go build -v -o bin/korb-linux-amd64
GOOS=darwin GOARCH=amd64 go build -v -o bin/korb-darwin-amd64

mover:
cd mover/ && docker build -t beryju/korb-mover .
docker push beryju/korb-mover

all: build mover
Empty file added README.md
Empty file.
62 changes: 62 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cmd

import (
"fmt"
"os"
"path/filepath"

"github.com/BeryJu/korb/pkg/config"
"github.com/BeryJu/korb/pkg/migrator"
log "github.com/sirupsen/logrus"

"github.com/spf13/cobra"
"k8s.io/client-go/util/homedir"
)

var kubeConfig string

var pvcNewStorageClass string
var pvcNewSize string

var force bool

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "k8s-storage-mover",
Long: `Move data between Kubernetes PVCs on different Storage Classes.`,
Run: func(cmd *cobra.Command, args []string) {
m := migrator.New(kubeConfig)
m.Force = force

m.DestPVCSize = pvcNewSize
m.DestPVCStorageClass = pvcNewStorageClass
m.SourcePVCName = args[0]
m.Run()
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
log.SetLevel(log.DebugLevel)

if home := homedir.HomeDir(); home != "" {
rootCmd.Flags().StringVar(&kubeConfig, "kubeConfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeConfig file")
} else {
rootCmd.Flags().StringVar(&kubeConfig, "kubeConfig", "", "absolute path to the kubeconfig file")
}

rootCmd.Flags().StringVar(&pvcNewStorageClass, "new-pvc-storage-class", "", "Storage class to use for the new PVC. If empty, the storage class of the source will be used.")
rootCmd.Flags().StringVar(&pvcNewSize, "new-pvc-size", "", "Size for the new PVC. If empty, the size of the source will be used. Accepts formats like used in Kubernetes Manifests (Gi, Ti, ...)")

rootCmd.Flags().BoolVar(&force, "force", false, "Ignore warning which would normally halt the tool during validation.")

rootCmd.Flags().StringVar(&config.DockerImage, "docker-image", config.DockerImage, "Image to use for moving jobs")
}
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/BeryJu/korb

go 1.15

require (
github.com/imdario/mergo v0.3.11 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/sirupsen/logrus v1.2.0
github.com/spf13/cobra v1.1.1
github.com/spf13/viper v1.7.1
k8s.io/api v0.19.3
k8s.io/apimachinery v0.19.3
k8s.io/client-go v0.19.0
k8s.io/klog v1.0.0 // indirect
k8s.io/utils v0.0.0-20201104234853-8146046b121e // indirect
)
478 changes: 478 additions & 0 deletions go.sum

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions hack/test-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: source-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
selector:
matchLabels:
app: test-deployment
template:
metadata:
labels:
app: test-deployment
spec:
containers:
- name: test-deployment
image: ubuntu:latest
# Just spin & wait forever
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumeMounts:
- name: source-pvc
mountPath: /source
volumes:
- name: source-pvc
persistentVolumeClaim:
claimName: source-pvc
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/BeryJu/korb/cmd"

func main() {
cmd.Execute()
}
7 changes: 7 additions & 0 deletions mover/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM alpine:3.12

RUN apk add --no-cache rsync && rm -rf /var/cache/apk/*

VOLUME [ "/source", "/dest" ]

CMD [ "rsync", "-aHA", "--progress", "/source/", "/dest" ]
3 changes: 3 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package config

var DockerImage = "beryju/korb-mover:latest"
40 changes: 40 additions & 0 deletions pkg/migrator/destination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package migrator

import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (m *Migrator) GetDestPVCSize(fallback resource.Quantity) resource.Quantity {
var destSize resource.Quantity
if m.DestPVCSize != "" {
destSize = resource.MustParse(m.DestPVCSize)
} else {
destSize = fallback
}
return destSize
}

func (m *Migrator) GetDestinationPVCTemplate(sourcePVC *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
var sc *string
if m.DestPVCStorageClass != "" {
sc = &m.DestPVCStorageClass
}
destPVC := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: m.SourcePVCName,
Namespace: m.kNS,
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: sourcePVC.Spec.AccessModes,
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceName(v1.ResourceStorage): m.GetDestPVCSize(*sourcePVC.Spec.Resources.Requests.Storage()),
},
},
StorageClassName: sc,
},
}
return destPVC
}
77 changes: 77 additions & 0 deletions pkg/migrator/migrator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package migrator

import (
log "github.com/sirupsen/logrus"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

type Migrator struct {
SourcePVCName string

DestPVCStorageClass string
DestPVCSize string

Force bool

kConfig *rest.Config
kClient *kubernetes.Clientset
kNS string

log *log.Entry
}

func New(kubeconfigPath string) *Migrator {
m := &Migrator{
log: log.WithField("component", "migrator"),
}
if kubeconfigPath != "" {
m.log.WithField("kubeconfig", kubeconfigPath).Debug("Created client from kubeconfig")
cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&clientcmd.ConfigOverrides{})

// use the current context in kubeconfig
config, err := cc.ClientConfig()

if err != nil {
m.log.WithError(err).Panic("Failed to get client config")
}
m.kConfig = config
ns, _, err := cc.Namespace()
if err != nil {
m.log.WithError(err).Panic("Failed to get current namespace")
} else {
m.log.WithField("namespace", ns).Debug("Got current namespace")
m.kNS = ns
}
} else {
m.log.Panic("Kubeconfig cannot be empty")
}

// create the clientset
clientset, err := kubernetes.NewForConfig(m.kConfig)
if err != nil {
panic(err.Error())
}
m.kClient = clientset
return m
}

func (m *Migrator) Run() {
sourcePVC, compatibleStrategies := m.Validate()
m.log.Debug("Compatible Strategies:")
for _, compatibleStrategy := range compatibleStrategies {
m.log.Debug(compatibleStrategy.Description())
}
destTemplate := m.GetDestinationPVCTemplate(sourcePVC)
if len(compatibleStrategies) == 1 {
m.log.Debug("Only one compatible strategy, running")
err := compatibleStrategies[0].Do(sourcePVC, destTemplate)
if err != nil {
m.log.WithError(err).Warning("Failed to migrate")
}
}
}
46 changes: 46 additions & 0 deletions pkg/migrator/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package migrator

import (
"context"

"github.com/BeryJu/korb/pkg/strategies"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (m *Migrator) Validate() (*v1.PersistentVolumeClaim, []strategies.Strategy) {
pvc := m.validateSourcePVC()
controllers, err := m.getPVCControllers(pvc)
if err != nil {
m.log.WithError(err).Panic("Failed to get controllers")
}
baseStrategy := strategies.NewBaseStrategy(m.kConfig, m.kClient, m.kNS)
allStrategies := strategies.StrategyInstances(baseStrategy)
compatibleStrategies := make([]strategies.Strategy, 0)
for _, strategy := range allStrategies {
if strategy.CompatibleWithControllers(controllers...) {
compatibleStrategies = append(compatibleStrategies, strategy)
}
}
return pvc, compatibleStrategies
}

func (m *Migrator) validateSourcePVC() *v1.PersistentVolumeClaim {
pvc, err := m.kClient.CoreV1().PersistentVolumeClaims(m.kNS).Get(context.TODO(), m.SourcePVCName, metav1.GetOptions{})
if err != nil {
m.log.WithError(err).Panic("Failed to get Source PVC")
}
m.log.WithField("uid", pvc.UID).WithField("name", pvc.Name).Debug("Got Source PVC")
destPVCTemplate := m.GetDestinationPVCTemplate(pvc)
sourceSize := pvc.Spec.Resources.Requests.Storage()
destSize := destPVCTemplate.Spec.Resources.Requests.Storage()
if sourceSize.Cmp(*destSize) == 1 {
l := m.log.WithField("src-size", sourceSize.String()).WithField("destSize", destSize.String())
if m.Force {
l.Warning("Destination PVC is smaller than source, ignoring because force.")
} else {
l.Panic("Destination PVC is smaller than source.")
}
}
return pvc
}
26 changes: 26 additions & 0 deletions pkg/migrator/validateController.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package migrator

import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
)

func (m *Migrator) getPVCControllers(pvc *corev1.PersistentVolumeClaim) ([]interface{}, error) {
pods, err := m.getPVCPods(pvc)
if err != nil {
return nil, err
}

for _, pod := range pods {
for _, owner := range m.resolveOwner(pod.ObjectMeta, &appsv1.StatefulSet{}) {
switch owner.(type) {
case *appsv1.Deployment:
m.log.Debug("Found deployment")
case *appsv1.StatefulSet:
m.log.Debug("Found statefulset")
}
}
}

return nil, nil
}
22 changes: 22 additions & 0 deletions pkg/migrator/validateDeployments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package migrator

import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
)

func (m *Migrator) getPVCDeployments(pvc *corev1.PersistentVolumeClaim) ([]*appsv1.Deployment, error) {
pods, err := m.getPVCPods(pvc)
if err != nil {
return nil, err
}

affectedOwners := make([]*appsv1.Deployment, 0)
for _, pod := range pods {
for _, owner := range m.resolveOwner(pod.ObjectMeta, &appsv1.Deployment{}) {
affectedOwners = append(affectedOwners, owner.(*appsv1.Deployment))
}
}

return affectedOwners, nil
}
Loading

0 comments on commit 6a748b7

Please sign in to comment.