-
Notifications
You must be signed in to change notification settings - Fork 19
/
helm.go
253 lines (210 loc) · 7.6 KB
/
helm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
package helm
import (
"fmt"
"github.com/Aptomi/aptomi/pkg/event"
"github.com/Aptomi/aptomi/pkg/lang"
"github.com/Aptomi/aptomi/pkg/util"
"github.com/pmezard/go-difflib/difflib"
"gopkg.in/yaml.v2"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/helm/pkg/helm"
"strings"
)
var helmCodeTypes = []string{"helm", "aptomi/code/kubernetes-helm"}
// GetSupportedCodeTypes returns all code types for which this plugin is registered to
func (plugin *Plugin) GetSupportedCodeTypes() []string {
return helmCodeTypes
}
// Create implements creation of a new component instance in the cloud by deploying a Helm chart
func (plugin *Plugin) Create(cluster *lang.Cluster, deployName string, params util.NestedParameterMap, eventLog *event.Log) error {
return plugin.createOrUpdate(cluster, deployName, params, eventLog, true)
}
// Update implements update of an existing component instance in the cloud by updating parameters of a helm chart
func (plugin *Plugin) Update(cluster *lang.Cluster, deployName string, params util.NestedParameterMap, eventLog *event.Log) error {
return plugin.createOrUpdate(cluster, deployName, params, eventLog, true)
}
func (plugin *Plugin) createOrUpdate(cluster *lang.Cluster, deployName string, params util.NestedParameterMap, eventLog *event.Log, create bool) error {
cache, err := plugin.getClusterCache(cluster, eventLog)
if err != nil {
return err
}
releaseName := getHelmReleaseName(deployName)
chartRepo, chartName, chartVersion, err := getHelmReleaseInfo(params)
if err != nil {
return err
}
helmClient, err := cache.newHelmClient(eventLog)
if err != nil {
return err
}
chartPath, err := plugin.fetchChart(chartRepo, chartName, chartVersion)
if err != nil {
return err
}
helmParams, err := yaml.Marshal(params)
if err != nil {
return err
}
currRelease, err := helmClient.ReleaseContent(releaseName)
if err != nil && !strings.Contains(err.Error(), "not found") {
return fmt.Errorf("error while looking for Helm release %s: %s", releaseName, err)
}
if create {
if currRelease != nil {
// If a release already exists, let's just go ahead and update it
eventLog.WithFields(event.Fields{}).Infof("Release '%s' already exists. Updating it", releaseName)
} else {
eventLog.WithFields(event.Fields{
"release": releaseName,
"chart": chartName,
"path": chartPath,
"params": string(helmParams),
}).Infof("Installing Helm release '%s', chart '%s', cluster: '%s'", releaseName, chartName, cluster.Name)
_, err = helmClient.InstallRelease(chartPath, cache.namespace, helm.ReleaseName(releaseName), helm.ValueOverrides(helmParams), helm.InstallReuseName(true))
return err
}
}
eventLog.WithFields(event.Fields{
"release": releaseName,
"chart": chartName,
"path": chartPath,
"params": string(helmParams),
}).Infof("Updating Helm release '%s', chart '%s', cluster: '%s'", releaseName, chartName, cluster.Name)
newRelease, err := helmClient.UpdateRelease(releaseName, chartPath, helm.UpdateValueOverrides(helmParams))
if err != nil {
return err
}
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(currRelease.Release.Manifest),
B: difflib.SplitLines(newRelease.Release.Manifest),
FromFile: "Previous",
ToFile: "Current",
Context: 3,
})
if err != nil {
return fmt.Errorf("error while calculating diff between chart manifests for Helm release '%s', chart '%s', cluster: '%s'", releaseName, chartName, cluster.Name)
}
if len(diff) == 0 {
diff = "without changes"
} else {
diff = "with diff: \n\n" + diff
}
eventLog.WithFields(event.Fields{
"release": releaseName,
"chart": chartName,
"path": chartPath,
"params": string(helmParams),
}).Debugf("Updated Helm release '%s', chart '%s', cluster: '%s' %s", releaseName, chartName, cluster.Name, diff)
return err
}
// Destroy implements destruction of an existing component instance in the cloud by running "helm delete" on the corresponding helm chart
func (plugin *Plugin) Destroy(cluster *lang.Cluster, deployName string, params util.NestedParameterMap, eventLog *event.Log) error {
cache, err := plugin.getClusterCache(cluster, eventLog)
if err != nil {
return err
}
releaseName := getHelmReleaseName(deployName)
helmClient, err := cache.newHelmClient(eventLog)
if err != nil {
return err
}
eventLog.WithFields(event.Fields{
"release": releaseName,
}).Infof("Deleting Helm release '%s'", releaseName)
_, err = helmClient.DeleteRelease(releaseName, helm.DeletePurge(true))
return err
}
// Cleanup implements cleanup phase for the Helm plugin. It closes all created and cached Tiller tunnels.
func (plugin *Plugin) Cleanup() error {
var err error
plugin.cache.Range(func(key, value interface{}) bool {
if c, ok := value.(*clusterCache); ok {
c.tillerTunnel.Close()
} else {
panic(fmt.Sprintf("clusterCache expected in Plugin cache, but found: %v", c))
}
return true
})
return err
}
// Endpoints returns map from port type to url for all services of the current chart
// TODO: reduce cyclomatic complexity
func (plugin *Plugin) Endpoints(cluster *lang.Cluster, deployName string, params util.NestedParameterMap, eventLog *event.Log) (map[string]string, error) { // nolint: gocyclo
cache, err := plugin.getClusterCache(cluster, eventLog)
if err != nil {
return nil, err
}
kubeClient, err := cache.newKubeClient()
if err != nil {
return nil, err
}
client := kubeClient.CoreV1()
releaseName := getHelmReleaseName(deployName)
selector := labels.Set{"release": releaseName}.AsSelector().String()
options := meta.ListOptions{LabelSelector: selector}
endpoints := make(map[string]string)
// Check all corresponding services
services, err := client.Services(cache.namespace).List(options)
if err != nil {
return nil, err
}
kubeHost, err := cache.getKubeExternalAddress()
if err != nil {
return nil, err
}
for _, service := range services.Items {
if service.Spec.Type == "NodePort" {
for _, port := range service.Spec.Ports {
sURL := fmt.Sprintf("%s:%d", kubeHost, port.NodePort)
// todo(slukjanov): could we somehow detect real schema? I think no :(
if util.StringContainsAny(port.Name, "https") {
sURL = "https://" + sURL
} else if util.StringContainsAny(port.Name, "ui", "rest", "http", "grafana") {
sURL = "http://" + sURL
}
endpoints[port.Name] = sURL
}
}
}
// Find Istio Ingress service (how ingress itself exposed)
service, err := client.Services(cache.namespace).Get("istio-ingress", meta.GetOptions{})
if err != nil {
// return if there is no Istio deployed
if k8serrors.IsNotFound(err) {
return endpoints, nil
}
return nil, err
}
istioIngress := "<unresolved>"
if service.Spec.Type == "NodePort" {
for _, port := range service.Spec.Ports {
if port.Name == "http" {
istioIngress = fmt.Sprintf("%s:%d", kubeHost, port.NodePort)
}
}
}
// Check all corresponding istio ingresses
ingresses, err := kubeClient.ExtensionsV1beta1().Ingresses(cache.namespace).List(options)
if err != nil {
return nil, err
}
// todo(slukjanov): support more then one ingress / rule / path
for _, ingress := range ingresses.Items {
if class, ok := ingress.Annotations["kubernetes.io/ingress.class"]; !ok || class != "istio" {
continue
}
for _, rule := range ingress.Spec.Rules {
for _, path := range rule.HTTP.Paths {
pathStr := strings.Trim(path.Path, ".*")
if rule.Host == "" {
endpoints["ingress"] = "http://" + istioIngress + pathStr
} else {
endpoints["ingress"] = "http://" + rule.Host + pathStr
}
}
}
}
return endpoints, nil
}