Skip to content

Commit 6245f9e

Browse files
authored
Implement the validation that checks if every template file is up-to-date (open-telemetry#65)
This commit resolves the issue that the Helm chart maintainers need to manually compare the template files and the upstream manifest in order to determine which template files are outdated. Example output of the release code: ``` 2021/08/09 12:26:09 The OpenTelemetry Collector CRD update finished. It's up-to-date now. 2021/08/09 12:26:09 ATTENTION: opentelemetry-operator-leader-election-rolebinding configuration doesn't exist. Please create it in the file: rolebinding.yaml 2021/08/09 12:26:09 The values.yaml and Chart.yaml update finished. They are up-to-date now. 2021/08/09 12:26:09 ATTENTION: opentelemetry-operator-controller-manager configuration doesn't exist. Please create it in the file: deployment.yaml 2021/08/09 12:26:09 ATTENTION: validatingwebhookconfiguration.yaml file needs to be updated ```
1 parent 042bb3c commit 6245f9e

File tree

8 files changed

+504
-61
lines changed

8 files changed

+504
-61
lines changed

charts/opentelemetry-operator/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
apiVersion: v2
22
name: opentelemetry-operator
3-
version: 0.1.1
3+
version: 0.1.2
44
description: OpenTelemetry Operator Helm chart for Kubernetes
55
type: application
66
home: https://opentelemetry.io/
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
module main
1+
module github.com/opentelemetry-helm-charts/charts/opentelemetry-operator/release
22

33
go 1.16
44

5-
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
5+
require (
6+
github.com/stretchr/testify v1.7.0
7+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
8+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
18
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
29
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
311
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
412
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

charts/opentelemetry-operator/release/main.go

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ package main
22

33
import (
44
"errors"
5-
"gopkg.in/yaml.v3"
65
"io"
76
"log"
87
"net/http"
9-
"os"
10-
"strings"
8+
9+
"gopkg.in/yaml.v3"
1110
)
1211

1312
const (
@@ -17,24 +16,37 @@ const (
1716
chartYAMLPath = "../Chart.yaml"
1817
operatorRepoPath = "repository: quay.io/opentelemetry/opentelemetry-operator"
1918
kubeRBACProxyRepoPath = "repository: gcr.io/kubebuilder/kube-rbac-proxy"
20-
appVersion = "appVersion"
2119
)
2220

2321
// K8sObject describes the values a Kubernetes object typically has.
2422
type K8sObject struct {
25-
ApiVersion string `yaml:"apiVersion"`
26-
Kind string `yaml:"kind"`
27-
Metadata map[interface{}]interface{} `yaml:"metadata"`
28-
Spec map[interface{}]interface{} `yaml:"spec"`
29-
Status map[interface{}]interface{} `yaml:"status"`
23+
ApiVersion string `yaml:"apiVersion"`
24+
Kind string `yaml:"kind"`
25+
Metadata map[interface{}]interface{} `yaml:"metadata"`
26+
Spec map[interface{}]interface{} `yaml:"spec"`
27+
Status map[interface{}]interface{} `yaml:"status"`
28+
Rules []map[interface{}]interface{} `yaml:"rules"`
29+
RoleRef map[interface{}]interface{} `yaml:"roleRef"`
30+
Subjects []map[interface{}]interface{} `yaml:"subjects"`
31+
Webhooks []map[interface{}]interface{} `yaml:"webhooks"`
3032
}
3133

3234
// The main keeps the OpenTelemetry Operator (OTEL Operator) Helm chart configurations up-to-date with the upstream automatically.
3335
// First, it scrapes the latest OTEL Operator manifest from the OTEL Operator github:
3436
// https://github.com/open-telemetry/opentelemetry-operator.
3537
// Then, it will update the OpenTelemetry Collector CRD YAML file, which is the only CRD at this point.
36-
// At last, it will retrieve the latest image tag from the OTEL Operator manifest and update the values.yaml and Chart.yaml.
38+
// Next, it will retrieve the latest image tag from the OTEL Operator manifest and update the values.yaml and Chart.yaml.
39+
// At last, it will check if every template file is up-to-date with the manifest. If not, it will notify the Helm chart maintainers.
3740
func main() {
41+
// Get all the templates of the OTEL Operator Helm chart.
42+
templates, err := getTemplates()
43+
if err != nil {
44+
panic(err)
45+
}
46+
47+
// needUpdate records if there is any template file needs to be updated.
48+
needUpdate := false
49+
3850
// Get the OTEL Operator manifest data.
3951
resp, err := http.Get(operatorManifestURL)
4052
if err != nil {
@@ -45,6 +57,7 @@ func main() {
4557
dc := yaml.NewDecoder(resp.Body)
4658

4759
for {
60+
// current Kubernetes resource from the upstream manifest
4861
var curObject K8sObject
4962
err := dc.Decode(&curObject)
5063
if errors.Is(err, io.EOF) {
@@ -56,65 +69,37 @@ func main() {
5669

5770
switch curObject.Kind {
5871
case "CustomResourceDefinition":
59-
// Update the OTEL Collector CRD YAML file.
60-
out, err := yaml.Marshal(curObject)
61-
if err != nil {
62-
panic(err)
63-
}
64-
err = os.WriteFile(collectorCRDPath, out, 0644)
72+
// Update the collector CRD template file.
73+
err = updateCRD(curObject, collectorCRDPath)
6574
if err != nil {
6675
panic(err)
6776
}
77+
6878
log.Println("The OpenTelemetry Collector CRD update finished. It's up-to-date now.")
6979

7080
case "Deployment":
71-
// Retrieve the latest image repository and tag of the two container images.
72-
managerImage := strings.Split(curObject.Spec["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["image"].(string), ":")
73-
kubeRBACProxyImage := strings.Split(curObject.Spec["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})[1].(map[string]interface{})["image"].(string), ":")
74-
75-
// Replace the image tags in values.yaml and appVersion in Chart.yaml with the latest ones.
76-
valuesFile, err := os.ReadFile(valuesYAMLPath)
77-
if err != nil {
78-
panic(err)
79-
}
80-
81-
valuesFileLines := strings.Split(string(valuesFile), "\n")
82-
for i, line := range valuesFileLines {
83-
if strings.Contains(line, operatorRepoPath) {
84-
valuesFileLines[i+1] = " tag: " + managerImage[1]
85-
} else if strings.Contains(line, kubeRBACProxyRepoPath) {
86-
valuesFileLines[i+1] = " tag: " + kubeRBACProxyImage[1]
87-
}
88-
}
89-
90-
valuesFileOutput := strings.Join(valuesFileLines, "\n")
91-
err = os.WriteFile(valuesYAMLPath, []byte(valuesFileOutput), 0644)
92-
if err != nil {
93-
panic(err)
94-
}
95-
96-
chartFile, err := os.ReadFile(chartYAMLPath)
81+
// Update the values.yaml and Chart.yaml with the latest image tags.
82+
err = updateImageTags(curObject, valuesYAMLPath, chartYAMLPath)
9783
if err != nil {
9884
panic(err)
9985
}
10086

101-
chartFileLines := strings.Split(string(chartFile), "\n")
102-
for i, line := range chartFileLines {
103-
if strings.Contains(line, appVersion) {
104-
chartFileLines[i] = appVersion + ": " + managerImage[1][1:]
105-
}
106-
}
87+
log.Println("The values.yaml and Chart.yaml update finished. They are up-to-date now.")
10788

108-
chartFileOutput := strings.Join(chartFileLines, "\n")
109-
err = os.WriteFile(chartYAMLPath, []byte(chartFileOutput), 0644)
89+
// Update the templates since we have made changes to values.yaml.
90+
templates, err = getTemplates()
11091
if err != nil {
11192
panic(err)
11293
}
11394

114-
log.Println("The values.yaml and Chart.yaml update finished. They are up-to-date now.")
95+
needUpdate = checkTemplate(curObject, templates) || needUpdate
11596

11697
default:
117-
continue
98+
needUpdate = checkTemplate(curObject, templates) || needUpdate
11899
}
119100
}
101+
102+
if !needUpdate {
103+
log.Println("All the template files are up-to-date.")
104+
}
120105
}
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
# Prerequisites
2+
3+
- [ ] Make sure you have installed Helm v3 or later versions. See [Helm website](https://helm.sh/docs/helm/helm_install/)
4+
for installation information.
5+
16
# Checklist
27

38
- [ ] Change directory to `opentelemetry-helm-charts/charts/opentelemetry-operator/release`. `cd ./charts/opentelemetry-operator/release` should be helpful.
4-
- [ ] Run the command `go run main.go` to update the OTEL Collector CRD, `values.yaml` and `Chart.yaml`
5-
- [ ] Download the latest OTEL Operator manifest from this [link](https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml).
6-
As you can see, there are several YAML files in this manifest separated by `---`.
7-
- [ ] Starting from the third YAML file, compare the YAML files in the manifest and the ones in the `charts/opentelemetry-operator/templates` directory.
8-
The names of YAML files under `templates` directory are the same as the value of key `kind` of the YAML files in the manifest.
9-
- [ ] Update our template YAML files to maintain consistency with the ones in the manifest (especially be careful with `role.yaml` and `clusterrole.yaml`).
9+
- [ ] Run the command `go run .` to update the OTEL Collector CRD, `values.yaml` and `Chart.yaml`and detect if any template file needs to be updated
10+
- [ ] If you see any template files need to be updated, update them to maintain consistency with the ones in the manifest (especially be careful with `role.yaml` and `clusterrole.yaml`). \
1011
Create a new YAML file under `templates` directory if it doesn't exist.
1112
Use `{{ template "opentelemetry-operator.name" . }}` to represent the name of OTEL Operator which probably is `opentelemetry-operator` in the manifest.
1213
Use `{{ template "opentelemetry-operator.namespace" . }}` to represent the namespace which probably is `opentelemetry-operator-system` in the manifest.
@@ -18,3 +19,4 @@
1819
- `values.yaml` stores the default values passed into the chart.
1920
- `Chart.yaml` contains all the basic information of the Helm chart.
2021
- `role.yaml` and `clusterrole.yaml` define what types of actions will be permitted.
22+
- `main.go` contains the release process code, `release.go` contains all the release functions
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log"
7+
"os"
8+
"os/exec"
9+
"reflect"
10+
"strings"
11+
12+
"gopkg.in/yaml.v3"
13+
)
14+
15+
const (
16+
operatorChartPath = ".."
17+
appVersion = "appVersion"
18+
)
19+
20+
// getTemplates renders all the template files in this Helm chart and stores them into a map whose keys are the
21+
// Kubernetes resource kind plus name and values are the Kubernetes resource itself.
22+
func getTemplates() (map[string]K8sObject, error) {
23+
templates := make(map[string]K8sObject)
24+
25+
// Use `helm template` command to render all the template files.
26+
cmd := exec.Command("helm", "template", operatorChartPath)
27+
out, err := cmd.Output()
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
// Process all the YAML files and store the key-value pairs into templates map.
33+
rawYAMLFiles := strings.Split(string(out), "---\n")
34+
for _, rawYAMLFile := range rawYAMLFiles {
35+
var curObject K8sObject
36+
err = yaml.Unmarshal([]byte(rawYAMLFile), &curObject)
37+
if err != nil {
38+
return nil, err
39+
}
40+
templates[fmt.Sprintf("%s#%v", curObject.Kind, curObject.Metadata["name"])] = curObject
41+
}
42+
43+
return templates, nil
44+
}
45+
46+
// updateCRD writes the latest OpenTelemetry Collector CRD template file to the Helm chart's Collector CRD file.
47+
func updateCRD(object K8sObject, collectorCRDPath string) error {
48+
out, err := yaml.Marshal(object)
49+
if err != nil {
50+
return err
51+
}
52+
53+
err = os.WriteFile(collectorCRDPath, out, 0644)
54+
if err != nil {
55+
return err
56+
}
57+
58+
return nil
59+
}
60+
61+
// updateImageTags retrieves the latest image tags and update the values.yaml and Chart.yaml respectively.
62+
func updateImageTags(object K8sObject, valuesYAMLPath string, chartYAMLPath string) error {
63+
var managerImageTag, kubeRBACProxyImageTag string
64+
65+
// Retrieve the latest image tags of the two container images.
66+
containers := object.Spec["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})
67+
68+
for i := range containers {
69+
image := containers[i].(map[string]interface{})["image"].(string)
70+
switch containers[i].(map[string]interface{})["name"].(string) {
71+
case "manager":
72+
managerImageTag = strings.Split(image, ":")[1]
73+
case "kube-rbac-proxy":
74+
kubeRBACProxyImageTag = strings.Split(image, ":")[1]
75+
}
76+
}
77+
78+
// Check whether both image tags are found.
79+
if managerImageTag == "" || kubeRBACProxyImageTag == "" {
80+
return errors.New("Error: could not find image tags of OpenTelemetry Operator")
81+
}
82+
83+
// Replace the image tags in values.yaml with the latest ones.
84+
valuesFile, err := os.ReadFile(valuesYAMLPath)
85+
if err != nil {
86+
return err
87+
}
88+
89+
valuesFileLines := strings.Split(string(valuesFile), "\n")
90+
for i, line := range valuesFileLines {
91+
if strings.Contains(line, operatorRepoPath) {
92+
valuesFileLines[i+1] = " tag: " + managerImageTag
93+
} else if strings.Contains(line, kubeRBACProxyRepoPath) {
94+
valuesFileLines[i+1] = " tag: " + kubeRBACProxyImageTag
95+
}
96+
}
97+
98+
valuesFileOutput := strings.Join(valuesFileLines, "\n")
99+
err = os.WriteFile(valuesYAMLPath, []byte(valuesFileOutput), 0644)
100+
if err != nil {
101+
return err
102+
}
103+
104+
// Update the appVersion in Chart.yaml.
105+
if err = updateAPPVersion(managerImageTag[1:], chartYAMLPath); err != nil {
106+
return err
107+
}
108+
109+
return nil
110+
}
111+
112+
// updateAPPVersion is a helper function of updateImageTags. It updates the appVersion in Chart.yaml with the given version.
113+
func updateAPPVersion(version string, chartYAMLPath string) error {
114+
chartFile, err := os.ReadFile(chartYAMLPath)
115+
if err != nil {
116+
return err
117+
}
118+
119+
chartFileLines := strings.Split(string(chartFile), "\n")
120+
for i, line := range chartFileLines {
121+
if strings.Contains(line, appVersion) {
122+
chartFileLines[i] = appVersion + ": " + version
123+
}
124+
}
125+
126+
chartFileOutput := strings.Join(chartFileLines, "\n")
127+
err = os.WriteFile(chartYAMLPath, []byte(chartFileOutput), 0644)
128+
if err != nil {
129+
return err
130+
}
131+
132+
return nil
133+
}
134+
135+
// checkTemplate checks if a given Kubernetes resource has a corresponding template file in this chart or not. If a counterpart exists,
136+
// it will also check if the two configuration files are the same.
137+
func checkTemplate(curObject K8sObject, templates map[string]K8sObject) bool {
138+
// Get the template file in this chart.
139+
templateObject := templates[fmt.Sprintf("%s#%v", curObject.Kind, curObject.Metadata["name"])]
140+
141+
if !reflect.DeepEqual(curObject, templateObject) {
142+
var filename string
143+
144+
switch curObject.Kind {
145+
case "Certificate", "Issuer":
146+
filename = "certmanager.yaml"
147+
default:
148+
filename = strings.ToLower(curObject.Kind) + ".yaml"
149+
}
150+
151+
if templateObject.Kind == "" {
152+
log.Printf("ATTENTION: %v configuration doesn't exist. Please create it in the file: %s", curObject.Metadata["name"], filename)
153+
} else {
154+
log.Printf("ATTENTION: %s file needs to be updated", filename)
155+
}
156+
157+
return true
158+
}
159+
160+
return false
161+
}

0 commit comments

Comments
 (0)