Skip to content
This repository was archived by the owner on Dec 11, 2021. It is now read-only.

Commit ba2283e

Browse files
committed
internal/pkg/scaffold/crd.go: overwrite CRD manifests for Go operators (operator-framework#1278)
* internal/pkg/scaffold: overwrite CRD's with newly generated ones unless operator is non-go * internal/pkg/scaffold/crd.go: set names if not empty, and set Repo to override PROJECT file check by the CRD generator * Gopkg.toml: add controller-tools override (temporary) * revendor * CHANGELOG.md: added kubebuilder annotation bug and change entries
1 parent 8121298 commit ba2283e

File tree

13 files changed

+235
-137
lines changed

13 files changed

+235
-137
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Renamed `--docker-build-args` option to `--image-build-args` option for `build` subcommand, because this option can now be shared with other image build tools than docker when `--image-builder` option is specified. ([#1311](https://github.com/operator-framework/operator-sdk/pull/1311))
1515
- Reduces Helm release information in CR status to only the release name and manifest and moves it from `status.conditions` to a new top-level `deployedRelease` field. ([#1309](https://github.com/operator-framework/operator-sdk/pull/1309))
1616
- **WARNING**: Users with active CRs and releases who are upgrading their helm-based operator should upgrade to one based on v0.7.0 before upgrading further. Helm operators based on v0.8.0+ will not seamlessly transition release state to the persistent backend, and will instead uninstall and reinstall all managed releases.
17+
- Go operator CRDs are overwritten when being regenerated by [`operator-sdk generate openapi`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#openapi). Users can now rely on `+kubebuilder` annotations in their API code, which provide access to most OpenAPIv3 [validation properties](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schema-object) (the full set will be supported in the near future, see [this PR](https://github.com/kubernetes-sigs/controller-tools/pull/190)) and [other CRD fields](https://book.kubebuilder.io/beyond_basics/generating_crd.html). ([#1278](https://github.com/operator-framework/operator-sdk/pull/1278))
1718

1819
### Deprecated
1920

@@ -24,6 +25,7 @@
2425
### Bug Fixes
2526

2627
- In Helm-based operators, when a custom resource with a failing release is reverted back to a working state, the `ReleaseFailed` condition is now correctly removed. ([#1321](https://github.com/operator-framework/operator-sdk/pull/1321))
28+
- [`operator-sdk generate openapi`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#openapi) no longer overwrites CRD values derived from `+kubebuilder` annotations in Go API code. See issues ([#1212](https://github.com/operator-framework/operator-sdk/issues/1212)) and ([#1323](https://github.com/operator-framework/operator-sdk/issues/1323)) for discussion. ([#1278](https://github.com/operator-framework/operator-sdk/pull/1278))
2729

2830
## v0.7.0
2931

Gopkg.lock

+2-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Gopkg.toml

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
name = "sigs.k8s.io/controller-runtime"
3131
version = "=v0.1.10"
3232

33+
# This override revision has a fix that allows CRD unit tests to run correctly.
34+
# Remove once v0.1.11 is released.
35+
[[override]]
36+
name = "sigs.k8s.io/controller-tools"
37+
revision = "9d55346c2bde73fb3326ac22eac2e5210a730207"
38+
3339
[[constraint]]
3440
name = "github.com/sergi/go-diff"
3541
version = "1.0.0"

internal/pkg/scaffold/crd.go

+78-67
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ package scaffold
1616

1717
import (
1818
"fmt"
19-
"io/ioutil"
20-
"os"
2119
"path/filepath"
2220
"strings"
2321
"sync"
@@ -41,6 +39,20 @@ type CRD struct {
4139

4240
// IsOperatorGo is true when the operator is written in Go.
4341
IsOperatorGo bool
42+
43+
once sync.Once
44+
fs afero.Fs // For testing, ex. afero.NewMemMapFs()
45+
}
46+
47+
func (s *CRD) initFS(fs afero.Fs) {
48+
s.once.Do(func() {
49+
s.fs = fs
50+
})
51+
}
52+
53+
func (s *CRD) getFS() afero.Fs {
54+
s.initFS(afero.NewOsFs())
55+
return s.fs
4456
}
4557

4658
func (s *CRD) GetInput() (input.Input, error) {
@@ -76,77 +88,78 @@ func initCache() {
7688
})
7789
}
7890

79-
func (s *CRD) SetFS(_ afero.Fs) {}
91+
var _ CustomRenderer = &CRD{}
92+
93+
func (s *CRD) SetFS(fs afero.Fs) { s.initFS(fs) }
8094

8195
func (s *CRD) CustomRender() ([]byte, error) {
82-
i, _ := s.GetInput()
83-
// controller-tools generates crd file names with no _crd.yaml suffix:
84-
// <group>_<version>_<kind>.yaml.
85-
path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1)
86-
87-
// controller-tools' generators read and make crds for all apis in pkg/apis,
88-
// so generate crds in a cached, in-memory fs to extract the data we need.
89-
if s.IsOperatorGo && !cache.fileExists(path) {
90-
g := &crdgenerator.Generator{
91-
RootPath: s.AbsProjectPath,
92-
Domain: strings.SplitN(s.Resource.FullGroup, ".", 2)[1],
93-
OutputDir: ".",
94-
SkipMapValidation: false,
95-
OutFs: cache,
96-
}
97-
if err := g.ValidateAndInitFields(); err != nil {
98-
return nil, err
99-
}
100-
if err := g.Do(); err != nil {
101-
return nil, err
102-
}
96+
i, err := s.GetInput()
97+
if err != nil {
98+
return nil, err
10399
}
104100

105-
dstCRD := newCRDForResource(s.Resource)
106-
// Get our generated crd's from the in-memory fs. If it doesn't exist in the
107-
// fs, the corresponding API does not exist yet, so scaffold a fresh crd
108-
// without a validation spec.
109-
// If the crd exists in the fs, and a local crd exists, append the validation
110-
// spec. If a local crd does not exist, use the generated crd.
111-
if _, err := cache.Stat(path); err != nil && !os.IsNotExist(err) {
112-
return nil, err
113-
} else if err == nil {
101+
crd := &apiextv1beta1.CustomResourceDefinition{}
102+
if s.IsOperatorGo {
103+
// controller-tools generates crd file names with no _crd.yaml suffix:
104+
// <group>_<version>_<kind>.yaml.
105+
path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1)
106+
107+
// controller-tools' generators read and make crds for all apis in pkg/apis,
108+
// so generate crds in a cached, in-memory fs to extract the data we need.
109+
if !cache.fileExists(path) {
110+
g := &crdgenerator.Generator{
111+
RootPath: s.AbsProjectPath,
112+
Domain: strings.SplitN(s.Resource.FullGroup, ".", 2)[1],
113+
Repo: s.Repo,
114+
OutputDir: ".",
115+
SkipMapValidation: false,
116+
OutFs: cache,
117+
}
118+
if err := g.ValidateAndInitFields(); err != nil {
119+
return nil, err
120+
}
121+
if err := g.Do(); err != nil {
122+
return nil, err
123+
}
124+
}
125+
114126
b, err := afero.ReadFile(cache, path)
115127
if err != nil {
116128
return nil, err
117129
}
118-
dstCRD = &apiextv1beta1.CustomResourceDefinition{}
119-
if err = yaml.Unmarshal(b, dstCRD); err != nil {
130+
if err = yaml.Unmarshal(b, crd); err != nil {
120131
return nil, err
121132
}
122-
val := dstCRD.Spec.Validation.DeepCopy()
123-
124-
// If the crd exists at i.Path, append the validation spec to its crd spec.
125-
if _, err := os.Stat(i.Path); err == nil {
126-
cb, err := ioutil.ReadFile(i.Path)
133+
// controller-tools does not set ListKind or Singular names.
134+
setCRDNamesForResource(crd, s.Resource)
135+
// Remove controller-tools default label.
136+
delete(crd.Labels, "controller-tools.k8s.io")
137+
} else {
138+
// There are currently no commands to update CRD manifests for non-Go
139+
// operators, so if a CRD manifests already exists for this gvk, this
140+
// scaffold is a no-op.
141+
path := filepath.Join(s.AbsProjectPath, i.Path)
142+
if _, err = s.getFS().Stat(path); err == nil {
143+
b, err := afero.ReadFile(s.getFS(), path)
127144
if err != nil {
128145
return nil, err
129146
}
130-
if len(cb) > 0 {
131-
dstCRD = &apiextv1beta1.CustomResourceDefinition{}
132-
if err = yaml.Unmarshal(cb, dstCRD); err != nil {
147+
if len(b) == 0 {
148+
crd = newCRDForResource(s.Resource)
149+
} else {
150+
if err = yaml.Unmarshal(b, crd); err != nil {
133151
return nil, err
134152
}
135-
dstCRD.Spec.Validation = val
136153
}
137154
}
138-
// controller-tools does not set ListKind or Singular names.
139-
dstCRD.Spec.Names = getCRDNamesForResource(s.Resource)
140-
// Remove controller-tools default label.
141-
delete(dstCRD.Labels, "controller-tools.k8s.io")
142155
}
143-
addCRDSubresource(dstCRD)
144-
addCRDVersions(dstCRD)
145-
return k8sutil.GetObjectBytes(dstCRD)
156+
157+
setCRDVersions(crd)
158+
return k8sutil.GetObjectBytes(crd)
146159
}
147160

148161
func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition {
149-
return &apiextv1beta1.CustomResourceDefinition{
162+
crd := &apiextv1beta1.CustomResourceDefinition{
150163
TypeMeta: metav1.TypeMeta{
151164
APIVersion: "apiextensions.k8s.io/v1beta1",
152165
Kind: "CustomResourceDefinition",
@@ -156,35 +169,33 @@ func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition {
156169
},
157170
Spec: apiextv1beta1.CustomResourceDefinitionSpec{
158171
Group: r.FullGroup,
159-
Names: getCRDNamesForResource(r),
160172
Scope: apiextv1beta1.NamespaceScoped,
161173
Version: r.Version,
162174
Subresources: &apiextv1beta1.CustomResourceSubresources{
163175
Status: &apiextv1beta1.CustomResourceSubresourceStatus{},
164176
},
165177
},
166178
}
179+
setCRDNamesForResource(crd, r)
180+
return crd
167181
}
168182

169-
func getCRDNamesForResource(r *Resource) apiextv1beta1.CustomResourceDefinitionNames {
170-
return apiextv1beta1.CustomResourceDefinitionNames{
171-
Kind: r.Kind,
172-
ListKind: r.Kind + "List",
173-
Plural: r.Resource,
174-
Singular: r.LowerKind,
183+
func setCRDNamesForResource(crd *apiextv1beta1.CustomResourceDefinition, r *Resource) {
184+
if crd.Spec.Names.Kind == "" {
185+
crd.Spec.Names.Kind = r.Kind
175186
}
176-
}
177-
178-
func addCRDSubresource(crd *apiextv1beta1.CustomResourceDefinition) {
179-
if crd.Spec.Subresources == nil {
180-
crd.Spec.Subresources = &apiextv1beta1.CustomResourceSubresources{}
187+
if crd.Spec.Names.ListKind == "" {
188+
crd.Spec.Names.ListKind = r.Kind + "List"
189+
}
190+
if crd.Spec.Names.Plural == "" {
191+
crd.Spec.Names.Plural = r.Resource
181192
}
182-
if crd.Spec.Subresources.Status == nil {
183-
crd.Spec.Subresources.Status = &apiextv1beta1.CustomResourceSubresourceStatus{}
193+
if crd.Spec.Names.Singular == "" {
194+
crd.Spec.Names.Singular = r.LowerKind
184195
}
185196
}
186197

187-
func addCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) {
198+
func setCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) {
188199
// crd.Version is deprecated, use crd.Versions instead.
189200
var crdVersions []apiextv1beta1.CustomResourceDefinitionVersion
190201
if crd.Spec.Version != "" {

internal/pkg/scaffold/crd_test.go

+54-24
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
package scaffold
1616

1717
import (
18-
"os"
1918
"path/filepath"
20-
"strings"
2119
"testing"
2220

23-
"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input"
21+
testutil "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/internal/testutil"
2422
"github.com/operator-framework/operator-sdk/internal/util/diffutil"
23+
"github.com/operator-framework/operator-sdk/internal/util/fileutil"
24+
25+
"github.com/spf13/afero"
2526
)
2627

2728
func TestCRDGoProject(t *testing.T) {
@@ -30,28 +31,18 @@ func TestCRDGoProject(t *testing.T) {
3031
t.Fatal(err)
3132
}
3233
s, buf := setupScaffoldAndWriter()
33-
absPath, err := os.Getwd()
34+
s.Fs = afero.NewMemMapFs()
35+
cfg, err := setupTestFrameworkConfig()
3436
if err != nil {
3537
t.Fatal(err)
3638
}
37-
// Set the project and repo paths to {abs}/test/test-framework, which
38-
// contains pkg/apis for the memcached-operator.
39-
tfDir := filepath.Join("test", "test-framework")
40-
pkgIdx := strings.Index(absPath, "internal/pkg")
41-
cfg := &input.Config{
42-
Repo: filepath.Join(absPath[strings.Index(absPath, "github.com"):pkgIdx], tfDir),
43-
AbsProjectPath: filepath.Join(absPath[:pkgIdx], tfDir),
44-
ProjectName: tfDir,
45-
}
46-
if err := os.Chdir(cfg.AbsProjectPath); err != nil {
39+
40+
err = testutil.WriteOSPathToFS(afero.NewOsFs(), s.Fs, cfg.AbsProjectPath)
41+
if err != nil {
4742
t.Fatal(err)
4843
}
49-
defer func() { os.Chdir(absPath) }()
50-
err = s.Execute(cfg, &CRD{
51-
Input: input.Input{Path: filepath.Join(tfDir, "cache_v1alpha1_memcached.yaml")},
52-
Resource: r,
53-
IsOperatorGo: true,
54-
})
44+
45+
err = s.Execute(cfg, &CRD{Resource: r, IsOperatorGo: true})
5546
if err != nil {
5647
t.Fatalf("Failed to execute the scaffold: (%v)", err)
5748
}
@@ -74,8 +65,6 @@ spec:
7465
plural: memcacheds
7566
singular: memcached
7667
scope: Namespaced
77-
subresources:
78-
status: {}
7968
validation:
8069
openAPIV3Schema:
8170
properties:
@@ -118,13 +107,31 @@ spec:
118107
`
119108

120109
func TestCRDNonGoProject(t *testing.T) {
110+
s, buf := setupScaffoldAndWriter()
111+
s.Fs = afero.NewMemMapFs()
112+
121113
r, err := NewResource(appApiVersion, appKind)
122114
if err != nil {
123115
t.Fatal(err)
124116
}
125-
s, buf := setupScaffoldAndWriter()
126-
err = s.Execute(appConfig, &CRD{Resource: r})
117+
118+
crd := &CRD{Resource: r}
119+
i, err := crd.GetInput()
127120
if err != nil {
121+
t.Fatal(err)
122+
}
123+
cfg, err := setupTestFrameworkConfig()
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
128+
path := filepath.Join(cfg.AbsProjectPath, i.Path)
129+
err = afero.WriteFile(s.Fs, path, []byte(crdNonGoExp), fileutil.DefaultFileMode)
130+
if err != nil {
131+
t.Fatal(err)
132+
}
133+
134+
if err = s.Execute(cfg, crd); err != nil {
128135
t.Fatalf("Failed to execute the scaffold: (%v)", err)
129136
}
130137

@@ -134,6 +141,9 @@ func TestCRDNonGoProject(t *testing.T) {
134141
}
135142
}
136143

144+
// crdNonGoExp contains a simple validation block to make sure manually-added
145+
// validation is not overwritten. Non-go projects don't have the luxury of
146+
// kubebuilder annotations.
137147
const crdNonGoExp = `apiVersion: apiextensions.k8s.io/v1beta1
138148
kind: CustomResourceDefinition
139149
metadata:
@@ -148,6 +158,26 @@ spec:
148158
scope: Namespaced
149159
subresources:
150160
status: {}
161+
validation:
162+
openAPIV3Schema:
163+
properties:
164+
spec:
165+
properties:
166+
size:
167+
format: int32
168+
type: integer
169+
required:
170+
- size
171+
type: object
172+
status:
173+
properties:
174+
nodes:
175+
items:
176+
type: string
177+
type: array
178+
required:
179+
- nodes
180+
type: object
151181
version: v1alpha1
152182
versions:
153183
- name: v1alpha1

internal/pkg/scaffold/gopkgtoml.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ required = [
6262
6363
[[override]]
6464
name = "sigs.k8s.io/controller-tools"
65-
version = "=v0.1.8"
65+
revision = "9d55346c2bde73fb3326ac22eac2e5210a730207"
6666
6767
[[override]]
6868
name = "k8s.io/api"

0 commit comments

Comments
 (0)