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

Add Helm 3 GetRelease #1406

Merged
merged 8 commits into from
Jan 8, 2020
Merged

Conversation

SimonAlling
Copy link
Contributor

@SimonAlling SimonAlling commented Dec 20, 2019

Description of the change

This PR implements GetRelease using the Helm 3 API.

The Dashboard doesn't understand the release format used by Helm 3, so we implemented a compatibility layer that translates Helm 3 releases to a Helm 2-similar format suitable for the Dashboard in its current form.

Benefits

  • By and large, it works.
  • The Dashboard need not be modified.

Possible drawbacks

  • The original values.yaml is not included in the response to the Dashboard, just a generated one without comments and formatting. The same goes for the actual config (values.yaml with release-specific modifications). We didn't know where to find the original files.
  • There are no tests yet (but there soon will be, thanks to @latiif).

Applicable issues

Additional information

Don't forget to set useHelm3 to true in values.yaml to see the changes.

Created in collaboration with @lindhe and @latiif.

As of this commit, the real values.yaml is NOT included in the response
to the Dashboard, just a placeholder one.
Files: compatibleFiles(h3c.Files),
Metadata: *h3c.Metadata,
Templates: compatibleTemplates(h3c.Templates),
Values: compatibleConfig(h3c),
Copy link
Contributor

@lindhe lindhe Dec 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to comments in the Helm code, "Config is the set of extra Values added to the chart. These values override the default values inside of the chart.".

I guess that means that we should not use compatibleConfig to set Values. So Config should be the subset of Values that we saw somewhere (you know that yaml that was stripped of comments...), but we should also have a compatibleValues() that returns the actual full Values contents (wherever that is to be found).

https://github.com/helm/helm/blob/master/pkg/release/release.go#L29-L31

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably right.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like config is the final, modified values. I deployed bitnami/apache with the replicaCount and metric.resources.limits properties manually modified in the GUI, and then compared values.raw with config.raw (both from the JSON response):

$ diff json-values.yaml json-config.yaml 
27d26
< 
51d49
< 
55d52
< 
58c55
< replicaCount: 1
---
> replicaCount: 5
86d82
< 
90d85
< 
94d88
< 
98d91
< 
164d156
< 
170d161
< 
179c170
<   secrets:
---
>   secrets: 
225,227c216,218
<     limits: {}
<     #   cpu: 100m
<     #   memory: 128Mi
---
>     limits:
>         cpu: 100m
>         memory: 128Mi

Note that both values.raw and config.raw preserved all YAML comments.

@lindhe
Copy link
Contributor

lindhe commented Dec 20, 2019

Could this be the function to use for getting the Values from a Release?

https://github.com/helm/helm/blob/master/pkg/chartutil/values.go#L137

They are not in their original raw YAML form, but instead generated from
the internal map[string]interface{} representation.
@SimonAlling
Copy link
Contributor Author

SimonAlling commented Jan 2, 2020

We've now implemented chart.values and config in the JSON response as best we can. We don't know where to find the original values.yaml (with comments etc), so we generate what YAML we can from the internal map[string]interface{} representation instead.

go.mod Show resolved Hide resolved
@SimonAlling
Copy link
Contributor Author

SimonAlling commented Jan 3, 2020

I've noticed that, when installing a chart, its Raw property contains the expected files, including values.yaml; however, when getting the resulting release, its Chart.Raw property is the empty list – at least as far as I can tell.

Copy link
Contributor

@absoludity absoludity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Simon. I think the approach here is correct: dashboard expects a json-serialized helm2 release, so you're creating one manually. But it seems this could be simplified by just using the helm2 structs (rather than creating new ones that are compatible) and using the helm2 code (ie. for the yaml serialization for the Raw field).

See below for more details and let me know if I missed something?

// generatedYamlHeader is prepended to YAML generated from the internal map[string]interface{} representation.
const generatedYamlHeader = "# Not original YAML! Generated from parsed representation."

type dashboardCompatibleRelease struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue using the h2.Release instead of creating a new struct here? It appears to have the same fields (extra ones serialize with omitempty):

https://github.com/kubeapps/kubeapps/blob/master/pkg/agent/agent_test.go#L185

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We originally wanted to write a function from h3.Release to h2.Release, but arrived at the conclusion that it's impossible in general. Can't remember exactly why, but I think there were several reasons. One might be that the respective Metadata types are not isomorphic. The Values property in the respective Chart types also seems problematic.

I've, however, made a best-effort attempt at using Helm 2 types instead of custom ones. It seems to work; see d67e136.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, here we're just looking at getting data back to the dashboard for presentation, rather than interacting with helm, so I don't think isomorphism is essential as it would be if we were generating this for use with helm itself. Thanks for the update!

Namespace string `json:"namespace,omitempty"`
}

type dashboardCompatibleChart struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here with https://github.com/helm/helm/blob/release-2.16/pkg/proto/hapi/chart/chart.pb.go#L24 (although I see you're using h3chart.Metadata below - are they the same?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, h2chart.Metadata and h3chart.Metadata are different (and not isomorphic).

Values dashboardCompatibleValues `json:"values,omitempty"`
}

type dashboardCompatibleValues struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and here https://github.com/helm/helm/blob/release-2.16/pkg/proto/hapi/chart/config.pb.go#L22

(in the helm2 code, the Values and Config appear to be the same thing, which looks reflected in you having the same type).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, Values and Config are not the same thing: Values is the original values.yaml for the chart, whereas Config is the effective set of values used for the release – usually Values overridden with any custom values set by the user at install time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry - yes, I meant that they appear to use the same type in helm2 rather than requiring two different but identical types - so Chart.Values (at https://github.com/helm/helm/blob/release-2.16/pkg/proto/hapi/chart/chart.pb.go#L32) is actually of type *Config (whereas here we had two identical types with dashboardCompatibleValues and dashboardCompatibleConfig I think?)

// valuesToYaml serializes to YAML and prepends an informative header.
// It assumes that the serialization succeeds.
func valuesToYaml(values map[string]interface{}) string {
marshaled, _ := yaml.Marshal(values)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to ignore the error rather than handling it here? This could result in hard-to-discover bugs later (ie. getting an empty values without any known reason why, I think?)

Actually, this looks like another place where we could just use the helm2 pkg? See

https://github.com/helm/helm/blob/bbdfe5e7803a12bbdf97e94cd847859890cf4050/pkg/tiller/release_server.go#L157

where Raw is set using https://github.com/helm/helm/blob/release-2.16/pkg/chartutil/values.go#L45

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought there would never occur any error there, and abandoning that assumption would contaminate the entire file with error and nil everywhere without adding any real value (or so I thought).

There should exist a total function from h3.Release to some Dashboard-compatible type, shouldn't there? (newDashboardCompatibleRelease is my attempt at constructing such a function.) Maybe @andresmgot's suggestion of storing the YAML separately is the best solution; then we wouldn't need to marshal anything at all.

Finally, I don't see how YAML could be used here. It works only on members of Values from Helm 2, but we have a map[string]interface{}.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the unlikely case that the map[string]interface{} couldn't be marshaled, returning an error here would be helpful to identify the issue. I would either log the error (if you want this function to succeed in any case, even if the values cannot be marshalled) or return it and handle it in an upper level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should exist a total function from h3.Release to some Dashboard-compatible type, shouldn't there? (newDashboardCompatibleRelease is my attempt at constructing such a function.) Maybe @andresmgot's suggestion of storing the YAML separately is the best solution; then we wouldn't need to marshal anything at all.

Yes, I agree - since the raw config is not stored in the helm3 model at all, yet is an integral part of the Kubeapps UI (which uses the values.yaml as the default config which users update), Kubeapps needs some way to access the raw user config (comments and all) as it was able to with helm2. I'm just not yet sure what the sanest way to do so is. I'm +1 to land this (once conflicts are resolved) and perhaps Andres or I can work on this aspect separately so as to not slow you down?

Finally, I don't see how YAML could be used here. It works only on members of Values from Helm 2, but we have a map[string]interface{}.

Values in the chartutil pkg is exactly that - map[string]interface{}. Two lines above the definition of func (v Values) YAML() you'll see:

type Values map[string]interface{}

It's a bit confusing because it's a different type to that used in the struct field Chart.Values. But as above, this is moot since we really need the raw data with comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest one of these options for now, to keep the code simple:

  • Panic if serialization failed.
  • Ignore any serialization error (current solution).

The rationale would be the same no matter which one we choose: This won't hit production anyway (because we need to figure out how to store/access values.yaml), and it's good enough™ for us to be able to keep working on Helm 3 support.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't you log the error instead? A panic can be disruptive in the whole process and you don't need to return the error and obscure the code if you just log the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable. I'll log it instead.

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR! Apart from the comments of @absoludity I see an important issue: We have no way of restoring the original values used (only the map representation). This means that comments are lost so when comparing these values when upgrading (a feature added in the latest version) the user will see all the comments being deleted.

It seems that information is not being stored by helm so we may want to add that information when creating a release (either as another field of the Secret/ConfigMap) or in a different entity so here we can return that instead of the plain map.

In any case that should not block this PR so lets continue.

Comment on lines 17 to 18
// generatedYamlHeader is prepended to YAML generated from the internal map[string]interface{} representation.
const generatedYamlHeader = "# Not original YAML! Generated from parsed representation."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would skip this header, then in the dashboard we can try to recover the comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the header should be there for clarity until we can do something better, such as storing the YAML separately, as you suggested.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the problem is that it's giving to the user internal information about a problem they may not be aware of. It seems like an error. It's easier to understand that the parsed YAML is what it's stored in the cluster (with no comments).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see!

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the end we have a translator package but we need to either do this translation here or in the dashboard so my +1 (with just one comment about the error). I would add a TODO to remove this package once we drop support for Helm v2.

Thanks! I will let @absoludity do a final review here as well.

// valuesToYaml serializes to YAML and prepends an informative header.
// It assumes that the serialization succeeds.
func valuesToYaml(values map[string]interface{}) string {
marshaled, _ := yaml.Marshal(values)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the unlikely case that the map[string]interface{} couldn't be marshaled, returning an error here would be helpful to identify the issue. I would either log the error (if you want this function to succeed in any case, even if the values cannot be marshalled) or return it and handle it in an upper level.

// valuesToYaml serializes to YAML and prepends an informative header.
// It assumes that the serialization succeeds.
func valuesToYaml(values map[string]interface{}) string {
marshaled, _ := yaml.Marshal(values)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should exist a total function from h3.Release to some Dashboard-compatible type, shouldn't there? (newDashboardCompatibleRelease is my attempt at constructing such a function.) Maybe @andresmgot's suggestion of storing the YAML separately is the best solution; then we wouldn't need to marshal anything at all.

Yes, I agree - since the raw config is not stored in the helm3 model at all, yet is an integral part of the Kubeapps UI (which uses the values.yaml as the default config which users update), Kubeapps needs some way to access the raw user config (comments and all) as it was able to with helm2. I'm just not yet sure what the sanest way to do so is. I'm +1 to land this (once conflicts are resolved) and perhaps Andres or I can work on this aspect separately so as to not slow you down?

Finally, I don't see how YAML could be used here. It works only on members of Values from Helm 2, but we have a map[string]interface{}.

Values in the chartutil pkg is exactly that - map[string]interface{}. Two lines above the definition of func (v Values) YAML() you'll see:

type Values map[string]interface{}

It's a bit confusing because it's a different type to that used in the struct field Chart.Values. But as above, this is moot since we really need the raw data with comments.

Copy link
Contributor

@andresmgot andresmgot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes!

@andresmgot andresmgot merged commit 4b4b14b into vmware-tanzu:master Jan 8, 2020
@SimonAlling SimonAlling deleted the helm3-get-release branch January 8, 2020 13:51
@SimonAlling SimonAlling mentioned this pull request Jan 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants