Repository: replicatedhq/kots
Pull request #240 (2020-01-07 22:42:10)

Limit ranges
This makes kots attempt to find a postgres and minio pvc size that's within acceptable limit ranges, if there are limit ranges defined.

For example, given a limit range of:

```yaml
% kubectl get limitrange resource-limits -o yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: resource-limits
  namespace: kots-tester
spec:
  limits:
  - max:
      cpu: "2"
      memory: 1Gi
    min:
      cpu: 7m
      memory: 100Mi
    type: Pod
  - default:
      cpu: "1"
      memory: 512Mi
    defaultRequest:
      cpu: 20m
      memory: 256Mi
    max:
      cpu: "2"
      memory: 1Gi
    min:
      cpu: 7m
      memory: 100Mi
    type: Container
  - max:
      storage: 1Gi
    min:
      storage: 1Gi
    type: PersistentVolumeClaim
```

When attempting to deploy, KOTS will prompt with:

```
Enter the namespace to deploy to: kots-tester
  • Deploying Admin Console
    • Creating namespace ✓  
✗ The storage request for minio is not acceptable for the current namespace. KOTS recommends a size of 4Gi, but will attempt to proceed with 1Gi to meet the namespace limits. Do you want to continue: 
```


Patch:
diff --git a/go.mod b/go.mod
index 5cd03bee6..4b3518053 100644
--- a/go.mod
+++ b/go.mod
@@ -19,9 +19,13 @@ require (
 	github.com/containers/storage v1.12.13 // indirect
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/docker/distribution v2.7.1+incompatible
+	github.com/docker/docker v1.13.1 // indirect
 	github.com/docker/docker-credential-helpers v0.6.3 // indirect
+	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.4.0
 	github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
+	github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
+	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect
 	github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect
 	github.com/etcd-io/bbolt v1.3.3 // indirect
@@ -29,6 +33,7 @@ require (
 	github.com/frankban/quicktest v1.4.1 // indirect
 	github.com/ghodss/yaml v1.0.0
 	github.com/golang/protobuf v1.3.2 // indirect
+	github.com/golang/snappy v0.0.1 // indirect
 	github.com/google/gofuzz v1.0.0
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/mux v1.7.3 // indirect
@@ -44,6 +49,8 @@ require (
 	github.com/mistifyio/go-zfs v2.1.1+incompatible // indirect
 	github.com/mtrmac/gpgme v0.0.0-20170102180018-b2432428689c // indirect
 	github.com/nicksnyder/go-i18n v0.0.0-00010101000000-000000000000 // indirect
+	github.com/nwaples/rardecode v1.0.0 // indirect
+	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
 	github.com/opencontainers/image-spec v1.0.1 // indirect
 	github.com/opencontainers/runc v1.0.0-rc8 // indirect
 	github.com/opencontainers/selinux v1.2.2 // indirect
@@ -64,6 +71,7 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.1.0 // indirect
+	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 	golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7
 	golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 // indirect
 	gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
diff --git a/pkg/kotsadm/kotsadm.go b/pkg/kotsadm/kotsadm.go
index 6747bd850..f9e098ebf 100644
--- a/pkg/kotsadm/kotsadm.go
+++ b/pkg/kotsadm/kotsadm.go
@@ -29,6 +29,7 @@ type DeployOptions struct {
 	NodePort               int32
 	Hostname               string
 	ApplicationMetadata    []byte
+	LimitRange             *corev1.LimitRange
 }
 
 type UpgradeOptions struct {
@@ -50,7 +51,7 @@ func YAML(deployOptions DeployOptions) (map[string][]byte, error) {
 		}
 	}
 
-	minioDocs, err := getMinioYAML(deployOptions.Namespace)
+	minioDocs, err := getMinioYAML(deployOptions)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get minio yaml")
 	}
@@ -58,7 +59,7 @@ func YAML(deployOptions DeployOptions) (map[string][]byte, error) {
 		docs[n] = v
 	}
 
-	postgresDocs, err := getPostgresYAML(deployOptions.Namespace, deployOptions.PostgresPassword)
+	postgresDocs, err := getPostgresYAML(deployOptions)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get postgres yaml")
 	}
@@ -183,6 +184,12 @@ func Deploy(deployOptions DeployOptions) error {
 		deployOptions.AutoCreateClusterToken = uuid.New().String()
 	}
 
+	limitRange, err := maybeGetNamespaceLimitRanges(clientset, deployOptions.Namespace)
+	if err != nil {
+		return errors.Wrap(err, "failed to get limit ranges for namespace")
+	}
+	deployOptions.LimitRange = limitRange
+
 	if err := ensureKotsadm(deployOptions, clientset, log); err != nil {
 		return errors.Wrap(err, "failed to deploy admin console")
 	}
diff --git a/pkg/kotsadm/limitrange.go b/pkg/kotsadm/limitrange.go
new file mode 100644
index 000000000..b278f3a32
--- /dev/null
+++ b/pkg/kotsadm/limitrange.go
@@ -0,0 +1,64 @@
+package kotsadm
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/manifoldco/promptui"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+func maybeGetNamespaceLimitRanges(clientset *kubernetes.Clientset, namespace string) (*corev1.LimitRange, error) {
+	limitRanges, err := clientset.CoreV1().LimitRanges(namespace).List(metav1.ListOptions{})
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to list limit ranges")
+	}
+
+	if len(limitRanges.Items) == 0 {
+		return nil, nil
+	}
+
+	return &limitRanges.Items[0], nil
+}
+
+func promptForSizeIfNotBetween(label string, desired *resource.Quantity, min *resource.Quantity, max *resource.Quantity) *resource.Quantity {
+	actualSize := desired
+
+	if max != nil {
+		if max.Cmp(*desired) == -1 {
+			/// desired is too big
+			actualSize = max
+		}
+	}
+	if min != nil {
+		if min.Cmp(*desired) == 1 {
+			/// desired is too small, yeap, you read that right
+			actualSize = min
+		}
+	}
+
+	if actualSize.Cmp(*desired) == 0 {
+		return desired
+	}
+
+	prompt := promptui.Prompt{
+		Label:     fmt.Sprintf("The storage request for %s is not acceptable for the current namespace. KOTS recommends a size of %s, but will attempt to proceed with %s to meet the namespace limits. Do you want to continue", label, desired.String(), actualSize.String()),
+		IsConfirm: true,
+	}
+
+	for {
+		_, err := prompt.Run()
+		if err != nil {
+			if err == promptui.ErrInterrupt {
+				os.Exit(-1)
+			}
+			continue
+		}
+
+		return actualSize
+	}
+}
diff --git a/pkg/kotsadm/minio.go b/pkg/kotsadm/minio.go
index 199042d56..681e22beb 100644
--- a/pkg/kotsadm/minio.go
+++ b/pkg/kotsadm/minio.go
@@ -11,18 +11,18 @@ import (
 	"k8s.io/client-go/kubernetes/scheme"
 )
 
-func getMinioYAML(namespace string) (map[string][]byte, error) {
+func getMinioYAML(deployOptions DeployOptions) (map[string][]byte, error) {
 	docs := map[string][]byte{}
 	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
 
 	var statefulset bytes.Buffer
-	if err := s.Encode(minioStatefulset(namespace), &statefulset); err != nil {
+	if err := s.Encode(minioStatefulset(deployOptions), &statefulset); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal minio statefulset")
 	}
 	docs["minio-statefulset.yaml"] = statefulset.Bytes()
 
 	var service bytes.Buffer
-	if err := s.Encode(minioService(namespace), &service); err != nil {
+	if err := s.Encode(minioService(deployOptions.Namespace), &service); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal minio service")
 	}
 	docs["minio-service.yaml"] = service.Bytes()
@@ -35,7 +35,7 @@ func ensureMinio(deployOptions DeployOptions, clientset *kubernetes.Clientset) e
 		return errors.Wrap(err, "failed to ensure minio secret")
 	}
 
-	if err := ensureMinioStatefulset(deployOptions.Namespace, clientset); err != nil {
+	if err := ensureMinioStatefulset(deployOptions, clientset); err != nil {
 		return errors.Wrap(err, "failed to ensure minio statefulset")
 	}
 
@@ -46,14 +46,14 @@ func ensureMinio(deployOptions DeployOptions, clientset *kubernetes.Clientset) e
 	return nil
 }
 
-func ensureMinioStatefulset(namespace string, clientset *kubernetes.Clientset) error {
-	_, err := clientset.AppsV1().StatefulSets(namespace).Get("kotsadm-minio", metav1.GetOptions{})
+func ensureMinioStatefulset(deployOptions DeployOptions, clientset *kubernetes.Clientset) error {
+	_, err := clientset.AppsV1().StatefulSets(deployOptions.Namespace).Get("kotsadm-minio", metav1.GetOptions{})
 	if err != nil {
 		if !kuberneteserrors.IsNotFound(err) {
 			return errors.Wrap(err, "failed to get existing statefulset")
 		}
 
-		_, err := clientset.AppsV1().StatefulSets(namespace).Create(minioStatefulset(namespace))
+		_, err := clientset.AppsV1().StatefulSets(deployOptions.Namespace).Create(minioStatefulset(deployOptions))
 		if err != nil {
 			return errors.Wrap(err, "failed to create minio statefulset")
 		}
diff --git a/pkg/kotsadm/minio_objects.go b/pkg/kotsadm/minio_objects.go
index b50f08348..a3cd149a3 100644
--- a/pkg/kotsadm/minio_objects.go
+++ b/pkg/kotsadm/minio_objects.go
@@ -2,17 +2,45 @@ package kotsadm
 
 import (
 	"fmt"
+	"os"
 
+	"github.com/replicatedhq/kots/pkg/util"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
-
-	"github.com/replicatedhq/kots/pkg/util"
 )
 
-func minioStatefulset(namespace string) *appsv1.StatefulSet {
+func minioStatefulset(deployOptions DeployOptions) *appsv1.StatefulSet {
+	size := resource.MustParse("4Gi")
+
+	if deployOptions.LimitRange != nil {
+		var allowedMax *resource.Quantity
+		var allowedMin *resource.Quantity
+
+		for _, limit := range deployOptions.LimitRange.Spec.Limits {
+			if limit.Type == corev1.LimitTypePersistentVolumeClaim {
+				max, ok := limit.Max["storage"]
+				if ok {
+					allowedMax = &max
+				}
+
+				min, ok := limit.Min["storage"]
+				if ok {
+					allowedMin = &min
+				}
+			}
+		}
+
+		newSize := promptForSizeIfNotBetween("minio", &size, allowedMin, allowedMax)
+		if newSize == nil {
+			os.Exit(-1)
+		}
+
+		size = *newSize
+	}
+
 	statefulset := &appsv1.StatefulSet{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "apps/v1",
@@ -20,7 +48,7 @@ func minioStatefulset(namespace string) *appsv1.StatefulSet {
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "kotsadm-minio",
-			Namespace: namespace,
+			Namespace: deployOptions.Namespace,
 		},
 		Spec: appsv1.StatefulSetSpec{
 			Selector: &metav1.LabelSelector{
@@ -42,7 +70,7 @@ func minioStatefulset(namespace string) *appsv1.StatefulSet {
 						},
 						Resources: corev1.ResourceRequirements{
 							Requests: corev1.ResourceList{
-								corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("4Gi"),
+								corev1.ResourceName(corev1.ResourceStorage): size,
 							},
 						},
 					},
diff --git a/pkg/kotsadm/postgres.go b/pkg/kotsadm/postgres.go
index bf3111058..02a7de3bd 100644
--- a/pkg/kotsadm/postgres.go
+++ b/pkg/kotsadm/postgres.go
@@ -12,21 +12,21 @@ import (
 	"k8s.io/client-go/kubernetes/scheme"
 )
 
-func getPostgresYAML(namespace string, password string) (map[string][]byte, error) {
+func getPostgresYAML(deployOptions DeployOptions) (map[string][]byte, error) {
 	docs := map[string][]byte{}
 	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
 
 	var statefulset bytes.Buffer
-	if password == "" {
-		password = uuid.New().String()
+	if deployOptions.PostgresPassword == "" {
+		deployOptions.PostgresPassword = uuid.New().String()
 	}
-	if err := s.Encode(postgresStatefulset(namespace), &statefulset); err != nil {
+	if err := s.Encode(postgresStatefulset(deployOptions), &statefulset); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal postgres statefulset")
 	}
 	docs["postgres-statefulset.yaml"] = statefulset.Bytes()
 
 	var service bytes.Buffer
-	if err := s.Encode(postgresService(namespace), &service); err != nil {
+	if err := s.Encode(postgresService(deployOptions.Namespace), &service); err != nil {
 		return nil, errors.Wrap(err, "failed to marshal postgres service")
 	}
 	docs["postgres-service.yaml"] = service.Bytes()
@@ -39,7 +39,7 @@ func ensurePostgres(deployOptions DeployOptions, clientset *kubernetes.Clientset
 		return errors.Wrap(err, "failed to ensure postgres secret")
 	}
 
-	if err := ensurePostgresStatefulset(deployOptions.Namespace, clientset); err != nil {
+	if err := ensurePostgresStatefulset(deployOptions, clientset); err != nil {
 		return errors.Wrap(err, "failed to ensure postgres statefulset")
 	}
 
@@ -50,14 +50,14 @@ func ensurePostgres(deployOptions DeployOptions, clientset *kubernetes.Clientset
 	return nil
 }
 
-func ensurePostgresStatefulset(namespace string, clientset *kubernetes.Clientset) error {
-	_, err := clientset.AppsV1().StatefulSets(namespace).Get("kotsadm-postgres", metav1.GetOptions{})
+func ensurePostgresStatefulset(deployOptions DeployOptions, clientset *kubernetes.Clientset) error {
+	_, err := clientset.AppsV1().StatefulSets(deployOptions.Namespace).Get("kotsadm-postgres", metav1.GetOptions{})
 	if err != nil {
 		if !kuberneteserrors.IsNotFound(err) {
 			return errors.Wrap(err, "failed to get existing statefulset")
 		}
 
-		_, err := clientset.AppsV1().StatefulSets(namespace).Create(postgresStatefulset(namespace))
+		_, err := clientset.AppsV1().StatefulSets(deployOptions.Namespace).Create(postgresStatefulset(deployOptions))
 		if err != nil {
 			return errors.Wrap(err, "failed to create postgres statefulset")
 		}
diff --git a/pkg/kotsadm/postgres_objects.go b/pkg/kotsadm/postgres_objects.go
index 103feb2e5..4d66ff40b 100644
--- a/pkg/kotsadm/postgres_objects.go
+++ b/pkg/kotsadm/postgres_objects.go
@@ -1,6 +1,8 @@
 package kotsadm
 
 import (
+	"os"
+
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -10,7 +12,35 @@ import (
 	"github.com/replicatedhq/kots/pkg/util"
 )
 
-func postgresStatefulset(namespace string) *appsv1.StatefulSet {
+func postgresStatefulset(deployOptions DeployOptions) *appsv1.StatefulSet {
+	size := resource.MustParse("1Gi")
+
+	if deployOptions.LimitRange != nil {
+		var allowedMax *resource.Quantity
+		var allowedMin *resource.Quantity
+
+		for _, limit := range deployOptions.LimitRange.Spec.Limits {
+			if limit.Type == corev1.LimitTypePersistentVolumeClaim {
+				max, ok := limit.Max["storage"]
+				if ok {
+					allowedMax = &max
+				}
+
+				min, ok := limit.Min["storage"]
+				if ok {
+					allowedMin = &min
+				}
+			}
+		}
+
+		newSize := promptForSizeIfNotBetween("minio", &size, allowedMin, allowedMax)
+		if newSize == nil {
+			os.Exit(-1)
+		}
+
+		size = *newSize
+	}
+
 	statefulset := &appsv1.StatefulSet{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: "apps/v1",
@@ -18,7 +48,7 @@ func postgresStatefulset(namespace string) *appsv1.StatefulSet {
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "kotsadm-postgres",
-			Namespace: namespace,
+			Namespace: deployOptions.Namespace,
 		},
 		Spec: appsv1.StatefulSetSpec{
 			Selector: &metav1.LabelSelector{
@@ -37,7 +67,7 @@ func postgresStatefulset(namespace string) *appsv1.StatefulSet {
 						},
 						Resources: corev1.ResourceRequirements{
 							Requests: corev1.ResourceList{
-								corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("1Gi"),
+								corev1.ResourceName(corev1.ResourceStorage): size,
 							},
 						},
 					},


Test patch:
diff --git a/pkg/kotsadm/postgres_test.go b/pkg/kotsadm/postgres_test.go
index 106bf4859..e97fd29ee 100644
--- a/pkg/kotsadm/postgres_test.go
+++ b/pkg/kotsadm/postgres_test.go
@@ -32,7 +32,10 @@ func Test_getPostgresYAML(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			req := require.New(t)
 
-			manifests, err := getPostgresYAML(test.namespace, test.password)
+			manifests, err := getPostgresYAML(DeployOptions{
+				Namespace:        test.namespace,
+				PostgresPassword: test.password,
+			})
 			req.NoError(err)
 			assert.NotNil(t, manifests)