Repository: replicatedhq/kots
Pull request #91 (2019-10-14 18:21:40)

Merge midstream kustomization on app updates


Patch:
diff --git a/kotskinds/apis/kots/v1beta1/license_types.go b/kotskinds/apis/kots/v1beta1/license_types.go
index 03fc845bb..60c78d46c 100644
--- a/kotskinds/apis/kots/v1beta1/license_types.go
+++ b/kotskinds/apis/kots/v1beta1/license_types.go
@@ -17,76 +17,16 @@ limitations under the License.
 package v1beta1
 
 import (
-	"encoding/json"
-	"strconv"
-
-	"github.com/pkg/errors"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-const (
-	Int Type = iota
-	String
-	Bool
-)
-
-type Type int
-
-type EntitlementValue struct {
-	Type    Type
-	IntVal  int64
-	StrVal  string
-	BoolVal bool
-}
-
-func (entitlementValue *EntitlementValue) Value() interface{} {
-	if entitlementValue.Type == Int {
-		return entitlementValue.IntVal
-	} else if entitlementValue.Type == Bool {
-		return entitlementValue.BoolVal
-	}
-
-	return entitlementValue.StrVal
-}
-
-func (entitlementValue *EntitlementValue) UnmarshalJSON(value []byte) error {
-	if value[0] == '"' {
-		entitlementValue.Type = String
-		return json.Unmarshal(value, &entitlementValue.StrVal)
-	}
-
-	intValue, err := strconv.ParseInt(string(value), 10, 64)
-	if err == nil {
-		entitlementValue.Type = Int
-		entitlementValue.IntVal = intValue
-		return nil
-	}
-
-	boolValue, err := strconv.ParseBool(string(value))
-	if err == nil {
-		entitlementValue.Type = Bool
-		entitlementValue.BoolVal = boolValue
-		return nil
-	}
-
-	return errors.New("unknown license value type")
-}
-
-type EntitlementField struct {
-	Title       string           `json:"title,omitempty"`
-	Description string           `json:"description,omitempty"`
-	Value       EntitlementValue `json:"value,omitempty"`
-}
-
 // LicenseSpec defines the desired state of LicenseSpec
 type LicenseSpec struct {
-	Signature         []byte                      `json:"signature"`
-	AppSlug           string                      `json:"appSlug"`
-	Endpoint          string                      `json:"endpoint,omitempty"`
-	LicenseID         string                      `json:"licenseID"`
-	IsAirgapSupported bool                        `json:"isAirgapSupported,omitempty"`
-	Entitlements      map[string]EntitlementField `json:"entitlements,omitempty"`
+	Signature         []byte `json:"signature"`
+	AppSlug           string `json:"appSlug"`
+	Endpoint          string `json:"endpoint,omitempty"`
+	LicenseID         string `json:"licenseID"`
+	IsAirgapSupported bool   `json:"isAirgapSupported,omitempty"`
 }
 
 // LicenseStatus defines the observed state of License
diff --git a/kotskinds/apis/kots/v1beta1/zz_generated.deepcopy.go b/kotskinds/apis/kots/v1beta1/zz_generated.deepcopy.go
index 2589c6f5a..229946b5e 100644
--- a/kotskinds/apis/kots/v1beta1/zz_generated.deepcopy.go
+++ b/kotskinds/apis/kots/v1beta1/zz_generated.deepcopy.go
@@ -387,37 +387,6 @@ func (in *ConfigValuesStatus) DeepCopy() *ConfigValuesStatus {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *EntitlementField) DeepCopyInto(out *EntitlementField) {
-	*out = *in
-	out.Value = in.Value
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EntitlementField.
-func (in *EntitlementField) DeepCopy() *EntitlementField {
-	if in == nil {
-		return nil
-	}
-	out := new(EntitlementField)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *EntitlementValue) DeepCopyInto(out *EntitlementValue) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EntitlementValue.
-func (in *EntitlementValue) DeepCopy() *EntitlementValue {
-	if in == nil {
-		return nil
-	}
-	out := new(EntitlementValue)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Installation) DeepCopyInto(out *Installation) {
 	*out = *in
@@ -574,13 +543,6 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) {
 		*out = make([]byte, len(*in))
 		copy(*out, *in)
 	}
-	if in.Entitlements != nil {
-		in, out := &in.Entitlements, &out.Entitlements
-		*out = make(map[string]EntitlementField, len(*in))
-		for key, val := range *in {
-			(*out)[key] = val
-		}
-	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LicenseSpec.
diff --git a/pkg/image/builder.go b/pkg/image/builder.go
index 870699343..ba34c9572 100644
--- a/pkg/image/builder.go
+++ b/pkg/image/builder.go
@@ -11,15 +11,15 @@ import (
 	"strings"
 
 	"github.com/containers/image/copy"
+	imagedocker "github.com/containers/image/docker"
 	"github.com/containers/image/signature"
 	"github.com/containers/image/transports/alltransports"
 	"github.com/docker/distribution/reference"
+	"github.com/docker/distribution/registry/api/errcode"
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"gopkg.in/yaml.v2"
-	imagedocker "github.com/containers/image/docker"
-	"github.com/docker/distribution/registry/api/errcode"
 )
 
 var imagePolicy = []byte(`{
diff --git a/pkg/k8sutil/portforward.go b/pkg/k8sutil/portforward.go
index 291cb259a..112e0869c 100644
--- a/pkg/k8sutil/portforward.go
+++ b/pkg/k8sutil/portforward.go
@@ -108,8 +108,8 @@ func PortForward(kubeContext string, localPort int, remotePort int, namespace st
 		}
 
 		time.Sleep(time.Millisecond * 100)
-		if (quickClient.Timeout < time.Second) {
-			quickClient.Timeout = quickClient.Timeout + time.Millisecond * 100
+		if quickClient.Timeout < time.Second {
+			quickClient.Timeout = quickClient.Timeout + time.Millisecond*100
 		}
 	}
 
diff --git a/pkg/midstream/lists.go b/pkg/midstream/lists.go
new file mode 100644
index 000000000..09df14765
--- /dev/null
+++ b/pkg/midstream/lists.go
@@ -0,0 +1,60 @@
+package midstream
+
+import (
+	"sigs.k8s.io/kustomize/v3/pkg/image"
+	kustomizetypes "sigs.k8s.io/kustomize/v3/pkg/types"
+)
+
+func findNewImages(new []image.Image, existing []image.Image) []image.Image {
+	newImages := make([]image.Image, 0)
+	names := make(map[string]bool)
+
+	for _, e := range existing {
+		names[e.Name] = true
+	}
+
+	for _, n := range new {
+		if _, exists := names[n.Name]; !exists {
+			names[n.Name] = true
+			newImages = append(newImages, n)
+		}
+	}
+
+	return newImages
+}
+
+func findNewPatches(new []kustomizetypes.PatchStrategicMerge, existing []kustomizetypes.PatchStrategicMerge) []kustomizetypes.PatchStrategicMerge {
+	newPatches := make([]kustomizetypes.PatchStrategicMerge, 0)
+	names := make(map[string]bool)
+
+	for _, e := range existing {
+		names[string(e)] = true
+	}
+
+	for _, n := range new {
+		if _, exists := names[string(n)]; !exists {
+			names[string(n)] = true
+			newPatches = append(newPatches, n)
+		}
+	}
+
+	return newPatches
+}
+
+func findNewStrings(new []string, existing []string) []string {
+	newStrings := make([]string, 0)
+	names := make(map[string]bool)
+
+	for _, e := range existing {
+		names[e] = true
+	}
+
+	for _, n := range new {
+		if _, exists := names[n]; !exists {
+			names[n] = true
+			newStrings = append(newStrings, n)
+		}
+	}
+
+	return newStrings
+}
diff --git a/pkg/midstream/midstream.go b/pkg/midstream/midstream.go
index fbe234a0e..0ded43a45 100644
--- a/pkg/midstream/midstream.go
+++ b/pkg/midstream/midstream.go
@@ -1,9 +1,8 @@
 package midstream
 
 import (
-	"github.com/replicatedhq/kots/pkg/k8sdoc"
-
 	"github.com/replicatedhq/kots/pkg/base"
+	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/kustomize/v3/pkg/image"
 	kustomizetypes "sigs.k8s.io/kustomize/v3/pkg/types"
diff --git a/pkg/midstream/write.go b/pkg/midstream/write.go
index 2d19c49a9..f515db8e5 100644
--- a/pkg/midstream/write.go
+++ b/pkg/midstream/write.go
@@ -1,7 +1,6 @@
 package midstream
 
 import (
-	"fmt"
 	"io/ioutil"
 	"os"
 	"path"
@@ -10,15 +9,15 @@ import (
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	"github.com/replicatedhq/kots/pkg/k8sutil"
-	corev1 "k8s.io/api/core/v1"
 	yaml "gopkg.in/yaml.v2"
+	corev1 "k8s.io/api/core/v1"
 	kustomizetypes "sigs.k8s.io/kustomize/v3/pkg/types"
 	k8syaml "sigs.k8s.io/yaml"
 )
 
 const (
-	secretFilename = "./secret.yaml"
-	objectsDir     = "./objects"
+	secretFilename  = "secret.yaml"
+	patchesFilename = "pullsecrets.yaml"
 )
 
 type WriteOptions struct {
@@ -26,11 +25,20 @@ type WriteOptions struct {
 	BaseDir      string
 }
 
+func (m *Midstream) KustomizationFilename(options WriteOptions) string {
+	return path.Join(options.MidstreamDir, "kustomization.yaml")
+}
+
 func (m *Midstream) WriteMidstream(options WriteOptions) error {
-	_, err := os.Stat(options.MidstreamDir)
+	var existingKustomization *kustomizetypes.Kustomization
+
+	_, err := os.Stat(m.KustomizationFilename(options))
 	if err == nil {
-		// no error, the midstream already exists
-		return nil
+		k, err := k8sutil.ReadKustomizationFromFile(m.KustomizationFilename(options))
+		if err != nil {
+			return errors.Wrap(err, "load existing kustomization")
+		}
+		existingKustomization = k
 	}
 
 	if err := os.MkdirAll(options.MidstreamDir, 0744); err != nil {
@@ -39,21 +47,23 @@ func (m *Midstream) WriteMidstream(options WriteOptions) error {
 
 	secretFilename, err := m.writePullSecret(options)
 	if err != nil {
-		return errors.Wrap(err, "failed to write kustomization")
+		return errors.Wrap(err, "failed to write secret")
 	}
 
 	if secretFilename != "" {
 		m.Kustomization.Resources = append(m.Kustomization.Resources, secretFilename)
 	}
 
-	objectPatches, err := m.writeObjectsWithPullSecret(options)
+	patchFilename, err := m.writeObjectsWithPullSecret(options)
 	if err != nil {
-		return errors.Wrap(err, "failed to write kustomization")
+		return errors.Wrap(err, "failed to write patches")
 	}
-	for _, patch := range objectPatches {
-		m.Kustomization.PatchesStrategicMerge = append(m.Kustomization.PatchesStrategicMerge, kustomizetypes.PatchStrategicMerge(patch))
+	if patchFilename != "" {
+		m.Kustomization.PatchesStrategicMerge = append(m.Kustomization.PatchesStrategicMerge, kustomizetypes.PatchStrategicMerge(patchFilename))
 	}
 
+	m.mergeKustomization(existingKustomization)
+
 	if err := m.writeKustomization(options); err != nil {
 		return errors.Wrap(err, "failed to write kustomization")
 	}
@@ -61,13 +71,28 @@ func (m *Midstream) WriteMidstream(options WriteOptions) error {
 	return nil
 }
 
+func (m *Midstream) mergeKustomization(existing *kustomizetypes.Kustomization) {
+	if existing == nil {
+		return
+	}
+
+	newImages := findNewImages(m.Kustomization.Images, existing.Images)
+	m.Kustomization.Images = append(existing.Images, newImages...)
+
+	newPatches := findNewPatches(m.Kustomization.PatchesStrategicMerge, existing.PatchesStrategicMerge)
+	m.Kustomization.PatchesStrategicMerge = append(existing.PatchesStrategicMerge, newPatches...)
+
+	newResources := findNewStrings(m.Kustomization.Resources, existing.Resources)
+	m.Kustomization.Resources = append(existing.Resources, newResources...)
+}
+
 func (m *Midstream) writeKustomization(options WriteOptions) error {
 	relativeBaseDir, err := filepath.Rel(options.MidstreamDir, options.BaseDir)
 	if err != nil {
 		return errors.Wrap(err, "failed to determine relative path for base from midstream")
 	}
 
-	fileRenderPath := path.Join(options.MidstreamDir, "kustomization.yaml")
+	fileRenderPath := m.KustomizationFilename(options)
 
 	m.Kustomization.Bases = []string{
 		relativeBaseDir,
@@ -85,72 +110,50 @@ func (m *Midstream) writePullSecret(options WriteOptions) (string, error) {
 		return "", nil
 	}
 
-	filename := filepath.Join(options.MidstreamDir, secretFilename)
-
-	// TODO: Overwrite or not?
-	_, err := os.Stat(filename)
-	if err == nil {
-		return secretFilename, nil
-	}
+	absFilename := filepath.Join(options.MidstreamDir, secretFilename)
 
 	b, err := k8syaml.Marshal(m.PullSecret)
 	if err != nil {
 		return "", errors.Wrap(err, "failed to marshal pull secret")
 	}
 
-	if err := ioutil.WriteFile(filename, b, 0644); err != nil {
+	if err := ioutil.WriteFile(absFilename, b, 0644); err != nil {
 		return "", errors.Wrap(err, "failed to write pull secret file")
 	}
 
 	return secretFilename, nil
 }
 
-func (m *Midstream) writeObjectsWithPullSecret(options WriteOptions) ([]string, error) {
-	dir := filepath.Join(options.MidstreamDir, objectsDir)
+func (m *Midstream) writeObjectsWithPullSecret(options WriteOptions) (string, error) {
+	if len(m.DocForPatches) == 0 {
+		return "", nil
+	}
 
-	// TODO: Overwrite or not?
-	// _, err := os.Stat(dir)
-	// if err == nil {
-	// 	return nil
-	// }
+	filename := filepath.Join(options.MidstreamDir, patchesFilename)
 
-	if _, err := os.Stat(dir); os.IsNotExist(err) {
-		if err := os.MkdirAll(dir, 0744); err != nil {
-			return nil, errors.Wrap(err, "failed to mkdir")
-		}
+	f, err := os.Create(filename)
+	if err != nil {
+		return "", errors.Wrap(err, "failed to create resources file")
 	}
+	defer f.Close()
 
-	resources := make([]string, 0)
 	for _, o := range m.DocForPatches {
-		err := func() error {
-			resource := filepath.Join(objectsDir, fmt.Sprintf("%s.yaml", o.Metadata.Name))
-			resources = append(resources, resource)
-
-			filename := filepath.Join(options.MidstreamDir, resource)
-
-			f, err := os.Create(filename)
-			if err != nil {
-				return errors.Wrap(err, "failed to craete resources file")
-			}
-			defer f.Close()
-
-			withPullSecret := obejctWithPullSecret(o, m.PullSecret)
-
-			b, err := yaml.Marshal(withPullSecret)
-			if err != nil {
-				return errors.Wrap(err, "failed to marshal object")
-			}
-			if _, err := f.Write(b); err != nil {
-				return errors.Wrap(err, "failed to write object")
-			}
-			return nil
-		}()
+		withPullSecret := obejctWithPullSecret(o, m.PullSecret)
+
+		b, err := yaml.Marshal(withPullSecret)
 		if err != nil {
-			return nil, err
+			return "", errors.Wrap(err, "failed to marshal object")
+		}
+
+		if _, err := f.Write([]byte("---\n")); err != nil {
+			return "", errors.Wrap(err, "failed to write object")
+		}
+		if _, err := f.Write(b); err != nil {
+			return "", errors.Wrap(err, "failed to write object")
 		}
 	}
 
-	return resources, nil
+	return patchesFilename, nil
 }
 
 func obejctWithPullSecret(obj *k8sdoc.Doc, secret *corev1.Secret) *k8sdoc.Doc {
diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go
index 32cb3f1b3..68d64f0e6 100644
--- a/pkg/pull/pull.go
+++ b/pkg/pull/pull.go
@@ -172,10 +172,9 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 		// Rewrite private images
 		registryInfo := registryProxyEndpointFromLicense(fetchOptions.License)
 		findPrivateImagesOptions := upstream.FindPrivateImagesOptions{
-			RootDir:      pullOptions.RootDir,
-			CreateAppDir: pullOptions.CreateAppDir,
-			AppSlug:      fetchOptions.License.Spec.AppSlug,
-			// TODO: This should use command line "registry-endpoint" arg as the override
+			RootDir:                 pullOptions.RootDir,
+			CreateAppDir:            pullOptions.CreateAppDir,
+			AppSlug:                 fetchOptions.License.Spec.AppSlug,
 			ReplicatedRegistry:      registryInfo.Registry,
 			ReplicatedRegistryProxy: registryInfo.Proxy,
 			Log:                     log,
@@ -358,7 +357,7 @@ func pullSecretFromToken(registries *registryInfo, token string, namespace strin
 			Kind:       "Secret",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      "kotsadm-replicated-registry", // TODO: hardcode or generate the name?
+			Name:      "kotsadm-replicated-registry",
 			Namespace: namespace,
 		},
 		Type: corev1.SecretTypeDockerConfigJson,
diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go
index 9ac946d09..d87ae5424 100644
--- a/pkg/upstream/replicated.go
+++ b/pkg/upstream/replicated.go
@@ -15,7 +15,6 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	imagedocker "github.com/containers/image/docker"
 	dockerref "github.com/containers/image/docker/reference"
 	imagetypes "github.com/containers/image/types"
@@ -23,6 +22,7 @@ import (
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
 	"github.com/replicatedhq/kots/pkg/image"
+	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"github.com/replicatedhq/kots/pkg/template"
 	"github.com/replicatedhq/kots/pkg/util"


Test patch:
diff --git a/kotskinds/apis/kots/v1beta1/v1beta1tests/license_types_test.go b/kotskinds/apis/kots/v1beta1/v1beta1tests/license_types_test.go
deleted file mode 100644
index 0bf86479c..000000000
--- a/kotskinds/apis/kots/v1beta1/v1beta1tests/license_types_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package v1beta1tests
-
-import (
-	"testing"
-
-	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
-	kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"k8s.io/client-go/kubernetes/scheme"
-)
-
-func Test_License(t *testing.T) {
-	data := `apiVersion: kots.io/v1beta1
-kind: License
-metadata:
-  name: local
-spec:
-  licenseID: abcdef
-  appSlug: my-app
-  endpoint: 'http://localhost:30016'
-  entitlements:
-    expires_at:
-      title: Expiration
-      description: License Expiration
-      value: ""
-    has-product-2:
-      title: Has Product 2
-      value: "test"
-    is_vip:
-      title: Is VIP
-      value: false
-    num_seats:
-      title: Number Of Seats
-      value: 10
-    sdzf:
-      title: sdf
-      value: 1
-    test:
-      title: test
-      value: "123asd"
-  signature: IA==`
-
-	kotsscheme.AddToScheme(scheme.Scheme)
-	decode := scheme.Codecs.UniversalDeserializer().Decode
-	obj, gvk, err := decode([]byte(data), nil, nil)
-	require.NoError(t, err)
-
-	assert.Equal(t, "kots.io", gvk.Group)
-	assert.Equal(t, "v1beta1", gvk.Version)
-	assert.Equal(t, "License", gvk.Kind)
-
-	license := obj.(*kotsv1beta1.License)
-
-	assert.Equal(t, "abcdef", license.Spec.LicenseID)
-
-	entitlements := license.Spec.Entitlements
-	assert.Len(t, entitlements, 6)
-
-	expiresAt := entitlements["expires_at"]
-	assert.NotNil(t, expiresAt)
-	assert.Equal(t, "Expiration", expiresAt.Title)
-	assert.Equal(t, "License Expiration", expiresAt.Description)
-
-	numSeats := entitlements["num_seats"]
-	assert.NotNil(t, numSeats)
-	assert.Equal(t, "Number Of Seats", numSeats.Title)
-	assert.Equal(t, "", numSeats.Description)
-
-	testField := entitlements["test"]
-	assert.NotNil(t, testField)
-	assert.Equal(t, "test", testField.Title)
-	assert.Equal(t, "123asd", testField.Value.Value())
-
-}
diff --git a/pkg/midstream/lists_test.go b/pkg/midstream/lists_test.go
new file mode 100644
index 000000000..74f3d933a
--- /dev/null
+++ b/pkg/midstream/lists_test.go
@@ -0,0 +1,36 @@
+package midstream
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_findNewStrings(t *testing.T) {
+	tests := []struct {
+		existingList []string
+		newList      []string
+		expected     []string
+	}{
+		{
+			existingList: []string{},
+			newList:      []string{"abc", "xyz"},
+			expected:     []string{"abc", "xyz"},
+		},
+		{
+			existingList: []string{"abc", "xyz"},
+			newList:      []string{},
+			expected:     []string{},
+		},
+		{
+			existingList: []string{"abc", "xyz", "ghi"},
+			newList:      []string{"abc", "def", "xyz"},
+			expected:     []string{"def"},
+		},
+	}
+
+	for _, test := range tests {
+		diff := findNewStrings(test.newList, test.existingList)
+		assert.Equal(t, test.expected, diff)
+	}
+}