github.com/jmrodri/operator-sdk@v0.5.0/pkg/scaffold/crd.go (about) 1 // Copyright 2018 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 scaffold 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "strings" 23 "sync" 24 25 "github.com/operator-framework/operator-sdk/pkg/scaffold/input" 26 27 "github.com/ghodss/yaml" 28 "github.com/spf13/afero" 29 apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime" 32 crdgenerator "sigs.k8s.io/controller-tools/pkg/crd/generator" 33 ) 34 35 // CRD is the input needed to generate a deploy/crds/<group>_<version>_<kind>_crd.yaml file 36 type CRD struct { 37 input.Input 38 39 // Resource defines the inputs for the new custom resource definition 40 Resource *Resource 41 42 // IsOperatorGo is true when the operator is written in Go. 43 IsOperatorGo bool 44 } 45 46 func (s *CRD) GetInput() (input.Input, error) { 47 if s.Path == "" { 48 fileName := fmt.Sprintf("%s_%s_%s_crd.yaml", 49 strings.ToLower(s.Resource.Group), 50 strings.ToLower(s.Resource.Version), 51 s.Resource.LowerKind) 52 s.Path = filepath.Join(CRDsDir, fileName) 53 } 54 initCache() 55 return s.Input, nil 56 } 57 58 type fsCache struct { 59 afero.Fs 60 } 61 62 func (c *fsCache) fileExists(path string) bool { 63 _, err := c.Stat(path) 64 return err == nil 65 } 66 67 var ( 68 // Global cache so users can use new CRD structs. 69 cache *fsCache 70 once sync.Once 71 ) 72 73 func initCache() { 74 once.Do(func() { 75 cache = &fsCache{Fs: afero.NewMemMapFs()} 76 }) 77 } 78 79 func (s *CRD) CustomRender() ([]byte, error) { 80 i, _ := s.GetInput() 81 // controller-tools generates crd file names with no _crd.yaml suffix: 82 // <group>_<version>_<kind>.yaml. 83 path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1) 84 85 // controller-tools' generators read and make crds for all apis in pkg/apis, 86 // so generate crds in a cached, in-memory fs to extract the data we need. 87 if s.IsOperatorGo && !cache.fileExists(path) { 88 g := &crdgenerator.Generator{ 89 RootPath: s.AbsProjectPath, 90 Domain: strings.SplitN(s.Resource.FullGroup, ".", 2)[1], 91 OutputDir: ".", 92 SkipMapValidation: false, 93 OutFs: cache, 94 } 95 if err := g.ValidateAndInitFields(); err != nil { 96 return nil, err 97 } 98 if err := g.Do(); err != nil { 99 return nil, err 100 } 101 } 102 103 dstCRD := newCRDForResource(s.Resource) 104 // Get our generated crd's from the in-memory fs. If it doesn't exist in the 105 // fs, the corresponding API does not exist yet, so scaffold a fresh crd 106 // without a validation spec. 107 // If the crd exists in the fs, and a local crd exists, append the validation 108 // spec. If a local crd does not exist, use the generated crd. 109 if _, err := cache.Stat(path); err != nil && !os.IsNotExist(err) { 110 return nil, err 111 } else if err == nil { 112 b, err := afero.ReadFile(cache, path) 113 if err != nil { 114 return nil, err 115 } 116 dstCRD = &apiextv1beta1.CustomResourceDefinition{} 117 if err = yaml.Unmarshal(b, dstCRD); err != nil { 118 return nil, err 119 } 120 val := dstCRD.Spec.Validation.DeepCopy() 121 122 // If the crd exists at i.Path, append the validation spec to its crd spec. 123 if _, err := os.Stat(i.Path); err == nil { 124 cb, err := ioutil.ReadFile(i.Path) 125 if err != nil { 126 return nil, err 127 } 128 if len(cb) > 0 { 129 dstCRD = &apiextv1beta1.CustomResourceDefinition{} 130 if err = yaml.Unmarshal(cb, dstCRD); err != nil { 131 return nil, err 132 } 133 dstCRD.Spec.Validation = val 134 } 135 } 136 // controller-tools does not set ListKind or Singular names. 137 dstCRD.Spec.Names = getCRDNamesForResource(s.Resource) 138 // Remove controller-tools default label. 139 delete(dstCRD.Labels, "controller-tools.k8s.io") 140 } 141 addCRDSubresource(dstCRD) 142 addCRDVersions(dstCRD) 143 return getCRDBytes(dstCRD) 144 } 145 146 func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition { 147 return &apiextv1beta1.CustomResourceDefinition{ 148 TypeMeta: metav1.TypeMeta{ 149 APIVersion: "apiextensions.k8s.io/v1beta1", 150 Kind: "CustomResourceDefinition", 151 }, 152 ObjectMeta: metav1.ObjectMeta{ 153 Name: r.Resource + "." + r.FullGroup, 154 }, 155 Spec: apiextv1beta1.CustomResourceDefinitionSpec{ 156 Group: r.FullGroup, 157 Names: getCRDNamesForResource(r), 158 Scope: apiextv1beta1.NamespaceScoped, 159 Version: r.Version, 160 Subresources: &apiextv1beta1.CustomResourceSubresources{ 161 Status: &apiextv1beta1.CustomResourceSubresourceStatus{}, 162 }, 163 }, 164 } 165 } 166 167 func getCRDNamesForResource(r *Resource) apiextv1beta1.CustomResourceDefinitionNames { 168 return apiextv1beta1.CustomResourceDefinitionNames{ 169 Kind: r.Kind, 170 ListKind: r.Kind + "List", 171 Plural: r.Resource, 172 Singular: r.LowerKind, 173 } 174 } 175 176 func addCRDSubresource(crd *apiextv1beta1.CustomResourceDefinition) { 177 if crd.Spec.Subresources == nil { 178 crd.Spec.Subresources = &apiextv1beta1.CustomResourceSubresources{} 179 } 180 if crd.Spec.Subresources.Status == nil { 181 crd.Spec.Subresources.Status = &apiextv1beta1.CustomResourceSubresourceStatus{} 182 } 183 } 184 185 func addCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) { 186 // crd.Version is deprecated, use crd.Versions instead. 187 var crdVersions []apiextv1beta1.CustomResourceDefinitionVersion 188 if crd.Spec.Version != "" { 189 var verExists, hasStorageVer bool 190 for _, ver := range crd.Spec.Versions { 191 if crd.Spec.Version == ver.Name { 192 verExists = true 193 } 194 // There must be exactly one version flagged as a storage version. 195 if ver.Storage { 196 hasStorageVer = true 197 } 198 } 199 if !verExists { 200 crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{ 201 {Name: crd.Spec.Version, Served: true, Storage: !hasStorageVer}, 202 } 203 } 204 } else { 205 crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{ 206 {Name: "v1alpha1", Served: true, Storage: true}, 207 } 208 } 209 210 if len(crd.Spec.Versions) > 0 { 211 // crd.Version should always be the first element in crd.Versions. 212 crd.Spec.Versions = append(crdVersions, crd.Spec.Versions...) 213 } else { 214 crd.Spec.Versions = crdVersions 215 } 216 } 217 218 func getCRDBytes(crd *apiextv1beta1.CustomResourceDefinition) ([]byte, error) { 219 // Remove the "status" field from yaml data, which causes a 220 // resource creation error. 221 crdMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(crd) 222 if err != nil { 223 return nil, err 224 } 225 delete(crdMap, "status") 226 return yaml.Marshal(&crdMap) 227 }