github.com/theishshah/operator-sdk@v0.6.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/internal/util/k8sutil" 26 "github.com/operator-framework/operator-sdk/pkg/scaffold/input" 27 28 "github.com/ghodss/yaml" 29 "github.com/spf13/afero" 30 apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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) SetFS(_ afero.Fs) {} 80 81 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 } 103 } 104 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 { 114 b, err := afero.ReadFile(cache, path) 115 if err != nil { 116 return nil, err 117 } 118 dstCRD = &apiextv1beta1.CustomResourceDefinition{} 119 if err = yaml.Unmarshal(b, dstCRD); err != nil { 120 return nil, err 121 } 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) 127 if err != nil { 128 return nil, err 129 } 130 if len(cb) > 0 { 131 dstCRD = &apiextv1beta1.CustomResourceDefinition{} 132 if err = yaml.Unmarshal(cb, dstCRD); err != nil { 133 return nil, err 134 } 135 dstCRD.Spec.Validation = val 136 } 137 } 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") 142 } 143 addCRDSubresource(dstCRD) 144 addCRDVersions(dstCRD) 145 return k8sutil.GetObjectBytes(dstCRD) 146 } 147 148 func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition { 149 return &apiextv1beta1.CustomResourceDefinition{ 150 TypeMeta: metav1.TypeMeta{ 151 APIVersion: "apiextensions.k8s.io/v1beta1", 152 Kind: "CustomResourceDefinition", 153 }, 154 ObjectMeta: metav1.ObjectMeta{ 155 Name: r.Resource + "." + r.FullGroup, 156 }, 157 Spec: apiextv1beta1.CustomResourceDefinitionSpec{ 158 Group: r.FullGroup, 159 Names: getCRDNamesForResource(r), 160 Scope: apiextv1beta1.NamespaceScoped, 161 Version: r.Version, 162 Subresources: &apiextv1beta1.CustomResourceSubresources{ 163 Status: &apiextv1beta1.CustomResourceSubresourceStatus{}, 164 }, 165 }, 166 } 167 } 168 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, 175 } 176 } 177 178 func addCRDSubresource(crd *apiextv1beta1.CustomResourceDefinition) { 179 if crd.Spec.Subresources == nil { 180 crd.Spec.Subresources = &apiextv1beta1.CustomResourceSubresources{} 181 } 182 if crd.Spec.Subresources.Status == nil { 183 crd.Spec.Subresources.Status = &apiextv1beta1.CustomResourceSubresourceStatus{} 184 } 185 } 186 187 func addCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) { 188 // crd.Version is deprecated, use crd.Versions instead. 189 var crdVersions []apiextv1beta1.CustomResourceDefinitionVersion 190 if crd.Spec.Version != "" { 191 var verExists, hasStorageVer bool 192 for _, ver := range crd.Spec.Versions { 193 if crd.Spec.Version == ver.Name { 194 verExists = true 195 } 196 // There must be exactly one version flagged as a storage version. 197 if ver.Storage { 198 hasStorageVer = true 199 } 200 } 201 if !verExists { 202 crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{ 203 {Name: crd.Spec.Version, Served: true, Storage: !hasStorageVer}, 204 } 205 } 206 } else { 207 crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{ 208 {Name: "v1alpha1", Served: true, Storage: true}, 209 } 210 } 211 212 if len(crd.Spec.Versions) > 0 { 213 // crd.Version should always be the first element in crd.Versions. 214 crd.Spec.Versions = append(crdVersions, crd.Spec.Versions...) 215 } else { 216 crd.Spec.Versions = crdVersions 217 } 218 }