Skip to content

Commit 6002c70

Browse files
author
Eric Stroczynski
authored
config3alphato3: convert config version 3-alpha to 3 (#4613)
internal/cmd/operator-sdk/alpha/config3alphato3: convert config version 3-alpha to 3 internal/cmd/operator-sdk/cli: add PersistentPreRun to root cmd for helpful error message Signed-off-by: Eric Stroczynski <[email protected]>
1 parent 025f0dd commit 6002c70

File tree

11 files changed

+646
-0
lines changed

11 files changed

+646
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
entries:
2+
- description: >
3+
PROJECT config version 3-alpha has been upgraded to version 3
4+
kind: change
5+
breaking: true
6+
migration:
7+
header: PROJECT config version 3-alpha must be upgraded to 3
8+
body: >
9+
PROJECT config version 3-alpha has been stabilized as
10+
[version 3](https://github.com/kubernetes-sigs/kubebuilder/blob/master/docs/book/src/migration/manually_migration_guide_v2_v3.md)
11+
(the `version` key in your PROJECT file), and contains a set of config fields sufficient
12+
to fully describe a project. While this change is not technically breaking
13+
because the spec at that version was alpha, it was used by default in `operator-sdk` commands
14+
so should be marked as breaking and have a convenient migration path.
15+
The `alpha config-3alpha-to-3` command will convert most of your PROJECT file
16+
from version 3-alpha to 3, and leave comments with directions where
17+
automatic conversion is not possible:
18+
19+
```console
20+
$ cat PROJECT
21+
version: 3-alpha
22+
resources:
23+
- crdVersion: v1
24+
...
25+
$ operator-sdk alpha config-3alpha-to-3
26+
Your PROJECT config file has been converted from version 3-alpha to 3. Please make sure all config data is correct.
27+
$ cat PROJECT
28+
version: "3"
29+
resources:
30+
- api:
31+
crdVersion: v1
32+
...
33+
```

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/spf13/pflag v1.0.5
2323
github.com/spf13/viper v1.7.0
2424
github.com/stretchr/testify v1.6.1
25+
golang.org/x/mod v0.3.0
2526
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e
2627
gomodules.xyz/jsonpatch/v3 v3.0.1
2728
helm.sh/helm/v3 v3.4.1
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package alpha
16+
17+
import (
18+
"github.com/spf13/cobra"
19+
20+
"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/alpha/config3alphato3"
21+
)
22+
23+
func NewCmd() *cobra.Command {
24+
cmd := &cobra.Command{
25+
Use: "alpha",
26+
Short: "Commands in alpha stage",
27+
Long: "Commands in alpha stage. These APIs are not stable and may be removed at any time.",
28+
}
29+
30+
cmd.AddCommand(
31+
config3alphato3.NewCmd(),
32+
)
33+
34+
return cmd
35+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package config3alphato3
16+
17+
import (
18+
"fmt"
19+
"io/ioutil"
20+
21+
log "github.com/sirupsen/logrus"
22+
"github.com/spf13/cobra"
23+
"sigs.k8s.io/yaml"
24+
)
25+
26+
func NewCmd() *cobra.Command {
27+
return &cobra.Command{
28+
Use: "config-3alpha-to-3",
29+
Short: "Convert your PROJECT config file from version 3-alpha to 3",
30+
Long: `Your PROJECT file contains config data specified by some version.
31+
This version is not a kubernetes-style version. In general, alpha and beta config versions
32+
are unstable and support for them is dropped once a stable version is released.
33+
The 3-alpha version has recently become stable (3), and therefore is no longer
34+
supported by operator-sdk v1.5+. This command is intended to migrate 3-alpha PROJECT files
35+
to 3 with as few manual modifications required as possible.
36+
`,
37+
RunE: func(cmd *cobra.Command, args []string) (err error) {
38+
cfgBytes, err := ioutil.ReadFile("PROJECT")
39+
if err != nil {
40+
return fmt.Errorf("%v (config-3alpha-to-3 must be run from project root)", err)
41+
}
42+
43+
if ver, err := getConfigVersion(cfgBytes); err == nil && ver != v3alpha {
44+
fmt.Println("Your PROJECT config file is not convertible at version", ver)
45+
return nil
46+
}
47+
48+
b, err := convertConfig3AlphaTo3(cfgBytes)
49+
if err != nil {
50+
return err
51+
}
52+
if err := ioutil.WriteFile("PROJECT", b, 0666); err != nil {
53+
return err
54+
}
55+
56+
fmt.Println("Your PROJECT config file has been converted from version 3-alpha to 3. " +
57+
"Please make sure all config data is correct.")
58+
59+
return nil
60+
},
61+
}
62+
}
63+
64+
// RootPersistentPreRun prints a helpful message on any exit caused by kubebuilder's
65+
// config unmarshal step finding "3-alpha", since the CLI will not recognize this version.
66+
// Add this to the root command (`operator-sdk`).
67+
var RootPersistentPreRun = func(cmd *cobra.Command, args []string) {
68+
if cfgBytes, err := ioutil.ReadFile("PROJECT"); err == nil {
69+
if ver, err := getConfigVersion(cfgBytes); err == nil && ver == v3alpha {
70+
log.Warn("Config version 3-alpha has been stabilized as 3, and 3-alpha is no longer supported. " +
71+
"Run `operator-sdk alpha config-3alpha-to-3` to upgrade your PROJECT config file to version 3",
72+
)
73+
}
74+
}
75+
}
76+
77+
func getConfigVersion(b []byte) (string, error) {
78+
var verObj struct {
79+
Version string `json:"version"`
80+
}
81+
return verObj.Version, yaml.Unmarshal(b, &verObj)
82+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package config3alphato3
16+
17+
import (
18+
"bufio"
19+
"bytes"
20+
"errors"
21+
"io/ioutil"
22+
"path"
23+
"regexp"
24+
"strings"
25+
"text/template"
26+
27+
"golang.org/x/mod/modfile"
28+
"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
29+
"sigs.k8s.io/yaml"
30+
)
31+
32+
var (
33+
v3alpha = "3-alpha"
34+
versionRe = regexp.MustCompile(`version:[ ]*(?:")?3-alpha(?:")?`)
35+
)
36+
37+
// convertConfig3AlphaTo3 returns cfgBytes converted to 3 iff cfgBytes is version 3-alpha.
38+
func convertConfig3AlphaTo3(cfgBytes []byte) (_ []byte, err error) {
39+
cfgObj := make(map[string]interface{}, 5)
40+
if err := yaml.Unmarshal(cfgBytes, &cfgObj); err != nil {
41+
return nil, err
42+
}
43+
44+
if version, hasVersion := cfgObj["version"]; !hasVersion || version.(string) != v3alpha {
45+
return cfgBytes, nil
46+
}
47+
48+
cfgBytes = versionRe.ReplaceAll(cfgBytes, []byte(`version: "3"`))
49+
50+
var modulePath string
51+
layout := cfgObj["layout"].(string)
52+
isGo := strings.HasPrefix(layout, "go.kubebuilder.io/")
53+
if isGo {
54+
if modulePath, err = getModulePath(); err != nil {
55+
return nil, err
56+
}
57+
}
58+
59+
multigroupObj, hasMultigroup := cfgObj["multigroup"]
60+
isMultigroup := hasMultigroup && multigroupObj.(bool)
61+
62+
if obj, hasRes := cfgObj["resources"]; hasRes {
63+
domain := cfgObj["domain"].(string)
64+
resObjs := obj.([]interface{})
65+
resources := make([]resource.Resource, len(resObjs))
66+
67+
for i, resObj := range resObjs {
68+
resources[i].Domain = domain
69+
70+
res := resObj.(map[string]interface{})
71+
if groupObj, ok := res["group"]; ok {
72+
resources[i].Group = groupObj.(string)
73+
}
74+
if versionObj, ok := res["version"]; ok {
75+
resources[i].Version = versionObj.(string)
76+
}
77+
if kindObj, ok := res["kind"]; ok {
78+
resources[i].Kind = kindObj.(string)
79+
}
80+
81+
if isGo {
82+
// Only Go projects use "resources[*].path".
83+
var apiPath string
84+
if isMultigroup {
85+
apiPath = path.Join("apis", resources[i].Group, resources[i].Version)
86+
} else {
87+
apiPath = path.Join("api", resources[i].Version)
88+
}
89+
resources[i].Path = path.Join(modulePath, apiPath)
90+
}
91+
92+
// Only set "resources[i].api" if "crdVersion" is present. Otherwise there is
93+
// likely no API defined by the project.
94+
if crdVersionObj, ok := res["crdVersion"]; ok {
95+
resources[i].API = &resource.API{
96+
CRDVersion: crdVersionObj.(string),
97+
}
98+
} else if k8sDomain, found := coreGroups[resources[i].Group]; found {
99+
// Core type case.
100+
resources[i].Domain = k8sDomain
101+
resources[i].Path = path.Join("k8s.io", "api", resources[i].Group, resources[i].Version)
102+
}
103+
// Only set "resources[i].webhooks" if "webhookVersion" is present. Otherwise there is
104+
// likely no webhook defined by the project.
105+
if whVersionObj, ok := res["webhookVersion"]; ok {
106+
resources[i].Webhooks = &resource.Webhooks{
107+
WebhookVersion: whVersionObj.(string),
108+
}
109+
}
110+
}
111+
112+
out := bytes.Buffer{}
113+
t := template.Must(template.New("").Parse(tmpl))
114+
if err := t.Execute(&out, resources); err != nil {
115+
return nil, err
116+
}
117+
118+
// Scan for resources, then replace only reources to preserve comments/order of the rest of PROJECT.
119+
scanner := bufio.NewScanner(bytes.NewBuffer(cfgBytes))
120+
start, end := -1, len(cfgBytes)
121+
for scanner.Scan() {
122+
text := scanner.Text()
123+
if strings.HasPrefix(text, "resources:") {
124+
start = bytes.Index(cfgBytes, []byte("resources:"))
125+
continue
126+
}
127+
if start != -1 && strings.Contains(text, ":") && !strings.HasPrefix(text, " ") {
128+
key := strings.TrimRightFunc(text, func(c rune) bool {
129+
return c != ':'
130+
})
131+
if _, hasKey := cfgObj[strings.TrimSuffix(key, ":")]; hasKey {
132+
end = bytes.Index(cfgBytes, []byte(text))
133+
break
134+
}
135+
}
136+
}
137+
138+
if start == -1 {
139+
return nil, errors.New("internal error: resources key not found in scanner")
140+
}
141+
cfgBytes = append(cfgBytes[:start], append(out.Bytes(), cfgBytes[end:]...)...)
142+
}
143+
144+
return cfgBytes, nil
145+
}
146+
147+
// Make this a var so it can be mocked in tests.
148+
var getModulePath = func() (string, error) {
149+
b, err := ioutil.ReadFile("go.mod")
150+
return modfile.ModulePath(b), err
151+
}
152+
153+
// Comment-heavy "resources" template.
154+
const tmpl = `resources:
155+
{{- range $i, $res := . }}
156+
-{{- if $res.API }} api:
157+
{{- if $res.API.CRDVersion }}
158+
crdVersion: {{ $res.API.CRDVersion }}
159+
{{- else }}
160+
# TODO(user): Change this API's CRD version if not v1.
161+
crdVersion: v1
162+
{{- end }}
163+
# TODO(user): Uncomment the below line if this resource's CRD is namespace scoped, else delete it.
164+
# namespaced: true
165+
{{- end }}
166+
# TODO(user): Uncomment the below line if this resource implements a controller, else delete it.
167+
# controller: true
168+
{{- if $res.Domain }}
169+
domain: {{ $res.Domain }}
170+
{{- end }}
171+
group: {{ $res.GVK.Group }}
172+
kind: {{ $res.GVK.Kind }}
173+
{{- if $res.Path }}
174+
# TODO(user): Update the package path for your API if the below value is incorrect.
175+
path: {{ $res.Path }}
176+
{{- end }}
177+
version: {{ $res.GVK.Version }}
178+
{{- if $res.Webhooks }}
179+
webhooks:
180+
# TODO(user): Uncomment the below line if this resource's webhook implements a conversion webhook, else delete it.
181+
# conversion: true
182+
# TODO(user): Uncomment the below line if this resource's webhook implements a defaulting webhook, else delete it.
183+
# defaulting: true
184+
# TODO(user): Uncomment the below line if this resource's webhook implements a validating webhook, else delete it.
185+
# validation: true
186+
{{- if $res.Webhooks.WebhookVersion }}
187+
webhookVersion: {{ $res.Webhooks.WebhookVersion }}
188+
{{- else }}
189+
# TODO(user): Change this API's webhook configuration version to the correct version if not v1.
190+
webhookVersion: v1
191+
{{- end }}
192+
{{- end }}
193+
{{- end }}
194+
`
195+
196+
// coreGroups maps a native k8s group to its domain. Several do not have a domain.
197+
var coreGroups = map[string]string{
198+
"admission": "k8s.io",
199+
"admissionregistration": "k8s.io",
200+
"apps": "",
201+
"auditregistration": "k8s.io",
202+
"apiextensions": "k8s.io",
203+
"authentication": "k8s.io",
204+
"authorization": "k8s.io",
205+
"autoscaling": "",
206+
"batch": "",
207+
"certificates": "k8s.io",
208+
"coordination": "k8s.io",
209+
"core": "",
210+
"events": "k8s.io",
211+
"extensions": "",
212+
"imagepolicy": "k8s.io",
213+
"networking": "k8s.io",
214+
"node": "k8s.io",
215+
"metrics": "k8s.io",
216+
"policy": "",
217+
"rbac.authorization": "k8s.io",
218+
"scheduling": "k8s.io",
219+
"setting": "k8s.io",
220+
"storage": "k8s.io",
221+
}

0 commit comments

Comments
 (0)